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
  
  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.