Suggest an editImprove this articleRefine the answer for “How to test an Express.js application?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Testing an Express.js application** means running automated HTTP checks against your routes using Supertest and Jest. ```js const request = require('supertest'); const app = require('./app'); // app.js exports Express, no listen() test('GET /users returns 200', async () => { const res = await request(app).get('/users'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('users'); }); ``` **Key point:** export your app without `listen()` so Supertest can inject it into an in-memory server. Mock the database layer for speed; test the full middleware chain for realism.Shown above the full answer for quick recall.Answer (EN)Image**Testing an Express.js application** means writing automated checks for your routes, middleware, and handlers using Supertest to simulate HTTP requests against a live app instance, without running a real server on a port. ## Theory ### TL;DR - Supertest acts as a fake HTTP client: it spins up an in-memory server, sends requests, and lets you assert on status codes, headers, and response body - Separate `app.js` (no `listen()`) from `server.js` (starts the server) - this single structural change makes everything else work - Unit tests mock dependencies; integration tests run the full app stack through real middleware chains - Use Supertest for any route test; mock the database layer after you confirm the HTTP wiring is correct - Async handlers without a global error middleware will cause silent test failures ### Quick example ```js // app.js - export the app, never call listen() here const express = require('express'); const app = express(); app.use(express.json()); app.get('/users', (req, res) => res.json({ users: [] })); module.exports = app; // app.test.js const request = require('supertest'); const app = require('./app'); test('GET /users returns 200', async () => { const res = await request(app).get('/users'); expect(res.status).toBe(200); // HTTP status expect(res.body).toEqual({ users: [] }); // response body }); ``` Supertest imports the app object directly, starts an in-memory server on a random port, and closes it after the test. No port conflicts, no `app.listen(3000)` needed. ### Key difference: unit tests vs. integration tests Unit tests call handler functions directly with mocked `req`/`res` objects. Fast, but they skip Express's middleware stack entirely. Integration tests with Supertest run the full request through every `app.use()` in order, which is how production traffic actually flows. For most Express bugs, the integration test catches what the unit test misses. ### When to use each approach - Single route behavior: Supertest, assert status and body - Middleware chain (auth, logging, validation): integration test with full pipeline - DB-dependent route: mock the service layer with `jest.mock()`, run Supertest on top - Protected routes: generate a real JWT in the test, pass it via the `Authorization` header - E2E user flows: Cypress or Playwright, not Supertest (Supertest has no browser) - CI/CD pipelines: Supertest only, no live ports needed ### How Supertest works internally When you call `request(app)`, Supertest passes your Express instance to Node's `http.createServer()` and binds it to a random high port (like 52341). The request travels through Express's actual router and middleware stack, V8 executes every callback, and the response pipes back through the test agent's socket. After the assertion, Supertest calls `server.close()`. No network I/O, no port 3000, no leftover server process. ### Test database strategy | Approach | Speed | Isolation | Realism | |---|---|---|---| | Mock service layer | Fastest | High | Low | | In-memory SQLite | Fast | Medium | Medium | | Dedicated test DB | Slower | Low (needs cleanup) | High | | Docker container | Slowest | High | Highest | Start with mocking the service layer. Once you have confidence the HTTP wiring is correct, add an in-memory DB for the critical paths. ### Common mistakes **Mistake: calling `app.listen()` inside `app.js`** ```js // Wrong - app.js binds port, blocks tests const app = express(); app.listen(3000); module.exports = app; // Fix: split into app.js (no listen) + server.js // server.js const app = require('./app'); app.listen(3000); ``` When `app.js` calls `listen()`, tests either fail with EADDRINUSE or Supertest binds a second port and requests never hit the right instance. **Mistake: missing `await` on Supertest** ```js // Wrong - test ends before response arrives test('GET /users', () => { request(app).get('/users').expect(200); // no await, no return }); // Fix test('GET /users', async () => { await request(app).get('/users').expect(200); }); ``` The test passes silently because Jest marks it done before the Promise resolves. This is one of the most common sources of false positives in Express test suites. **Mistake: async handlers without a global error middleware** ```js // app.js - crashes silently in tests app.get('/data', async (req, res) => { throw new Error('DB fail'); // unhandled, no response sent }); // Fix: add before module.exports app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); ``` Without the 4-argument error handler, Express 4 leaves the socket hanging and the test times out. About 40% of developers miss this specifically for async routes. **Mistake: not testing the full middleware chain for protected routes** ```js // Wrong - auth middleware never runs const handler = require('./handlers/profile'); handler({ user: mockUser }, res, next); // Fix - test through the full chain const token = jwt.sign({ id: 1 }, process.env.JWT_SECRET); await request(app) .get('/profile') .set('Authorization', `Bearer ${token}`) .expect(200); ``` **Mistake: real database in tests** Any test that writes to a shared DB makes your suite order-dependent and flaky. Use `jest.mock('./db')` or an in-memory alternative. A single dirty record from test A breaks test B in ways that are painful to debug. ### Real-world usage - NestJS uses the same pattern: `supertest(app.getHttpAdapter().getInstance())` - Express + Prisma: mock the Prisma client with `jest-mock-extended`, run Supertest on routes - Strapi CMS: wraps Supertest for testing custom API controllers - Microservice setup: 60% unit tests on handlers, 30% integration with Supertest and mocked Redis, 10% contract tests with Pact.js ### Follow-up questions **Q:** Why use Supertest instead of raw `http.request()`? **A:** Supertest chains `.expect()` assertions, auto-closes the server after each test, and handles JSON parsing automatically. Raw `http.request()` requires all of that manually. **Q:** How do you test middleware in isolation? **A:** Create a minimal Express app just for the test, mount only that middleware, and hit it with Supertest. This avoids interference from other middleware in the main app. **Q:** How do you mock external HTTP calls inside handlers? **A:** Use `nock`: `nock('https://api.stripe.com').post('/charges').reply(200, { id: 'ch_1' })`. It intercepts outgoing requests at the Node `http` layer before they reach the network. **Q:** What is the difference between `request(app)` and `request(server)`? **A:** `request(app)` creates a temporary in-memory server per test. `request(server)` connects to an already-running server on a real port. Use `request(app)` for integration tests and `request(server)` only if you need persistent state across a full test suite. **Q:** In a microservice with 100+ routes and Redis caching, how do you structure tests for 90% coverage without making each test slow? **A:** Layer it: 60% unit tests on handlers (Jest, no HTTP), 30% integration tests (Supertest with `ioredis-mock`), 10% contract tests (Pact.js for service boundaries). Run with `--maxWorkers=4` to parallelize. Seed test data with factory functions, not fixtures. This keeps median integration test time under 50ms. ## Examples ### Basic: testing a public route ```js // app.js const express = require('express'); const app = express(); app.use(express.json()); app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() }); }); module.exports = app; // health.test.js const request = require('supertest'); const app = require('./app'); test('GET /health returns status ok', async () => { const res = await request(app).get('/health'); expect(res.status).toBe(200); expect(res.body.status).toBe('ok'); expect(typeof res.body.uptime).toBe('number'); }); ``` A health check endpoint is usually the first test you write. It confirms Supertest is wired correctly and the app module exports cleanly. ### Intermediate: testing middleware and route together ```js // routes/users.js const router = require('express').Router(); // Timestamp middleware - runs before every handler in this router router.use((req, res, next) => { req.timestamp = Date.now(); next(); }); router.get('/', (req, res) => { res.json({ users: [], requestedAt: req.timestamp }); }); module.exports = router; // test/users.test.js const request = require('supertest'); const app = require('../app'); describe('Users API', () => { test('GET /users includes middleware timestamp', async () => { const res = await request(app).get('/users'); expect(res.status).toBe(200); expect(res.body.requestedAt).toBeDefined(); // middleware ran expect(res.body.requestedAt).toBeGreaterThan(0); // real timestamp expect(Array.isArray(res.body.users)).toBe(true); }); }); ``` This test confirms the middleware and route handler work together. A unit test on the handler alone would never catch a broken `next()` call in the middleware. ### Advanced: mocked service layer with auth and error handling ```js // Mock the DB service before importing the app jest.mock('../services/users.service', () => ({ findById: jest.fn(), create: jest.fn(), })); const request = require('supertest'); const jwt = require('jsonwebtoken'); const app = require('../app'); const usersService = require('../services/users.service'); const token = jwt.sign({ id: 1, role: 'admin' }, 'test-secret'); describe('POST /users', () => { test('creates user when authenticated', async () => { usersService.create.mockResolvedValue({ id: 5, name: 'Alice' }); const res = await request(app) .post('/users') .set('Authorization', `Bearer ${token}`) .send({ name: 'Alice', email: 'alice@example.com' }); expect(res.status).toBe(201); expect(res.body.name).toBe('Alice'); expect(usersService.create).toHaveBeenCalledWith({ name: 'Alice', email: 'alice@example.com', }); }); test('returns 500 when service throws', async () => { usersService.create.mockRejectedValue(new Error('DB connection lost')); const res = await request(app) .post('/users') .set('Authorization', `Bearer ${token}`) .send({ name: 'Bob', email: 'bob@example.com' }); expect(res.status).toBe(500); expect(res.body).toHaveProperty('error'); }); }); ``` The mock replaces the real DB call, so the test runs in under 10ms and never touches a database. The second test verifies the global error middleware catches async failures and returns a proper 500 instead of a hanging socket.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.