On this page
Testing Node.js
Testing backend code ensures APIs behave correctly and regressions are caught early.
Node.js Built-in Test Runner (Node 18+)
// math.test.js
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { add, divide } from './math.js';
describe('math', () => {
it('adds two numbers', () => {
assert.strictEqual(add(2, 3), 5);
});
it('throws on division by zero', () => {
assert.throws(() => divide(10, 0), /Division by zero/);
});
});
node --test math.test.js
node --test **/*.test.js # All tests
Vitest (Recommended)
npm install -D vitest supertest
package.json:
{ "scripts": { "test": "vitest", "test:run": "vitest run" } }
// math.test.js
import { describe, it, expect } from 'vitest';
import { add } from './math.js';
describe('add', () => {
it('returns sum', () => {
expect(add(1, 2)).toBe(3);
});
});
Testing Express APIs with Supertest
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import app from './app.js';
describe('GET /api/users', () => {
it('returns users array', async () => {
const res = await request(app).get('/api/users');
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
});
describe('POST /api/users', () => {
it('creates a user', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: '[email protected]' });
expect(res.status).toBe(201);
expect(res.body.name).toBe('Alice');
});
it('returns 400 for invalid data', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: '' });
expect(res.status).toBe(400);
});
});
Mocking Dependencies
import { vi, describe, it, expect } from 'vitest';
vi.mock('./db.js', () => ({
findUser: vi.fn().mockResolvedValue({ id: 1, name: 'Mock User' })
}));
import { findUser } from './db.js';
import { getUserHandler } from './handlers.js';
describe('getUserHandler', () => {
it('returns user from db', async () => {
const req = { params: { id: '1' } };
const res = { json: vi.fn(), status: vi.fn().mockReturnThis() };
await getUserHandler(req, res);
expect(findUser).toHaveBeenCalledWith('1');
expect(res.json).toHaveBeenCalledWith({ id: 1, name: 'Mock User' });
});
});
Test Database
Use a separate test database or in-memory MongoDB:
// setup.js
import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});
CI Integration
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
Aim for tests on all API endpoints and critical business logic.