intermediate

Testing Express with Supertest

Test Express routes through real HTTP request semantics while controlling dependencies, fixtures, auth state, and failure paths.

Supertest drives an Express app through real HTTP semantics without binding a port — it wraps `app` and asserts on status, headers, and body. This tests the full middleware stack, not isolated handler functions.

					import request from 'supertest';
import { app } from '../app';

it('creates a user', async () => {
  const res = await request(app)
    .post('/api/users')
    .set('Authorization', 'Bearer test-token')
    .send({ email: 'a@b.com', age: 25 })
    .expect(201);
  expect(res.body.id).toBeDefined();
});
				

Export the Express app without `listen()` for testability. Mock external dependencies (DB, queues) at module boundaries or inject fakes via factory. Test failure paths: 400 validation, 401 auth, 404 not found, 500 mapped errors.

On interviews: contrast Supertest integration tests with unit tests on services, how to seed fixtures, and avoiding flaky tests from shared mutable state.

Common pitfalls: calling `listen()` in the app module, sharing one DB without transactions or cleanup, and asserting only happy paths.

The trade-off is balancing simplicity, performance, safety, and operability — name which axis you optimized and what cost you accepted.

Checklist:

  • Export app separately from server bootstrap.
  • Mock or isolate external IO.
  • Cover auth, validation, and error responses.
  • Reset state between tests reliably.