Skip to content

Jest with Express.js

Testing an Express application means testing HTTP behavior: status codes, response bodies, and headers. This lesson covers combining Jest with Supertest to test Express routes without starting a real server on a real port.

Testing Routes with Supertest

Supertest wraps your Express app object directly, letting you make simulated HTTP requests against it in-process, without binding to an actual network port. This keeps route tests fast and avoids port conflicts in CI.

You export the Express app (without calling .listen() in the testable module) so both your real server entry point and your tests can import and use the same app instance.

// app.js
const express = require('express');
const app = express();

app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

module.exports = app;

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

test('GET /health returns a 200 status with ok status', async () => {
  const response = await request(app).get('/health');

  expect(response.status).toBe(200);
  expect(response.body).toEqual({ status: 'ok' });
});

Supertest makes a real in-process HTTP request against the Express app, without needing app.listen().

Testing POST Requests with a Body

const response = await request(app)
  .post('/users')
  .send({ name: 'Ada', email: 'ada@example.com' })
  .set('Accept', 'application/json');

expect(response.status).toBe(201);
expect(response.body).toMatchObject({ name: 'Ada' });
  • .send(body) attaches a JSON (or form) request body to POST/PUT/PATCH requests.
  • .set(header, value) sets custom request headers, like Authorization.
  • response.body is automatically parsed JSON when the response's content type is JSON.
  • Chain .expect(statusCode) directly for a shorthand status assertion, if preferred over response.status.

Jest + Express Cheat Sheet

Common Supertest patterns for testing Express apps.

Goal Pattern
GET request await request(app).get('/path')
POST with a body request(app).post('/path').send({...})
Set a header .set('Authorization', 'Bearer token')
Assert status code expect(response.status).toBe(200)
Assert JSON body expect(response.body).toEqual({...})
Chainable status assertion .expect(201)

Testing Middleware in Isolation

Middleware functions take (req, res, next). You can test simple middleware directly by calling it with mock req/res objects and a jest.fn() for next, without needing a full Express app or Supertest at all.

function requireAuth(req, res, next) {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
}

test('calls next() when authorized', () => {
  const req = { headers: { authorization: 'Bearer token' } };
  const res = { status: jest.fn(), json: jest.fn() };
  const next = jest.fn();

  requireAuth(req, res, next);

  expect(next).toHaveBeenCalled();
});

Testing Error-Handling Routes

Test both the success and failure paths of every route — an invalid input should return the correct status code (usually 400) and a helpful error message, and unexpected server errors should return a 500 without leaking internal details.

test('returns 400 for a missing email field', async () => {
  const response = await request(app).post('/users').send({ name: 'Ada' });

  expect(response.status).toBe(400);
  expect(response.body.error).toMatch('email');
});

Common Mistakes

  • Calling app.listen() inside the module used by tests, causing port conflicts across test files.
  • Testing only the happy path and skipping validation/error-handling routes entirely.
  • Testing middleware only through full HTTP requests when a direct unit test would be simpler.
  • Forgetting await before Supertest requests, since they return promises.

Key Takeaways

  • Supertest makes in-process HTTP requests against an Express app without a real network port.
  • Export your Express app separately from the code that calls .listen().
  • Middleware can be unit tested directly with mock req/res/next objects.
  • Always test both success and error-handling paths for every route.

Pro Tip

Structure your Express project so app.js exports the configured app and a separate server.js calls app.listen(). This single separation is what makes clean, port-free Supertest testing possible.