Skip to content

Integration Testing Express Routes

Integration tests verify that multiple pieces, middleware, routing, controllers, and (usually) a real or in-memory database, work correctly together. This lesson builds a complete integration test for a CRUD route.

Testing the Full Request Pipeline

Unlike unit tests, an integration test sends an actual request through your Express app, exercising every middleware and handler in the real chain, exactly as a real client would trigger it.

This catches bugs unit tests miss, wrong route registration order, missing middleware, incorrect status codes, that only show up when the pieces run together.

const request = require('supertest');
const app = require('../app');

describe('GET /users/:id', () => {
  test('returns 404 for a missing user', async () => {
    const res = await request(app).get('/users/999999');
    expect(res.status).toBe(404);
    expect(res.body.error).toBeDefined();
  });
});

This test doesn't know or care whether the 404 comes from a manual check or a database miss, it verifies the observable HTTP behavior.

Setting Up a Test Database

beforeAll(async () => {
  await connectTestDatabase();
});

afterEach(async () => {
  await clearTestDatabase();
});

afterAll(async () => {
  await disconnectTestDatabase();
});
  • Connect to a dedicated test database (or an in-memory one) before the suite runs.
  • Clear relevant collections/tables between tests so they don't interfere with each other.
  • Disconnect cleanly after the suite finishes to avoid hanging test processes.
  • Never point integration tests at a shared development or production database.

Integration Testing Cheatsheet

Common supertest assertions for testing full routes.

Check Code
Status code expect(res.status).toBe(201)
Response body expect(res.body).toMatchObject({ ... })
Response header expect(res.headers['content-type']).toMatch(/json/)
Send JSON body request(app).post('/users').send({ email })
Set headers request(app).get('/me').set('Authorization', Bearer ${token})

A Full CRUD Integration Test

A thorough integration test suite for a resource typically covers creation, successful reads, not-found reads, updates, and deletion, verifying both status codes and response shape.

describe('Users API', () => {
  test('creates a user', async () => {
    const res = await request(app)
      .post('/users')
      .send({ email: 'ada@example.com', name: 'Ada' });

    expect(res.status).toBe(201);
    expect(res.body).toMatchObject({ email: 'ada@example.com' });
  });

  test('rejects an invalid email', async () => {
    const res = await request(app)
      .post('/users')
      .send({ email: 'not-an-email' });

    expect(res.status).toBe(400);
  });
});

Testing Authenticated Routes

For routes protected by authentication, generate a valid token (or session) in a test setup step, then attach it to requests exactly as a real client would.

let token;

beforeAll(async () => {
  const res = await request(app).post('/login').send({ email, password });
  token = res.body.token;
});

test('GET /me requires authentication', async () => {
  const res = await request(app)
    .get('/me')
    .set('Authorization', `Bearer ${token}`);

  expect(res.status).toBe(200);
});

Common Mistakes

  • Running integration tests against a shared database used by other developers or environments.
  • Not cleaning up test data between test cases, causing tests to interfere with each other.
  • Testing implementation details (internal function calls) instead of observable HTTP behavior.
  • Skipping negative test cases like invalid input or missing authentication.

Key Takeaways

  • Integration tests exercise the full request pipeline, middleware, routing, and handlers together.
  • Always use a dedicated or in-memory test database, never a shared or production one.
  • Clean up test data between tests to keep them independent and repeatable.
  • Cover both success and failure scenarios, including authentication and validation failures.

Pro Tip

Group integration tests by resource (describe('Users API', ...)) and by scenario within that (creation, validation, not-found, auth), this structure makes failures immediately clear when a test run fails.