Testing Express Applications

1. Installing Testing Libraries

Example: vitest + supertest

npm i -D vitest supertest @vitest/coverage-v8
# or:
npm i -D jest supertest @types/jest
# Native Node 20+:
node --test --experimental-test-coverage

2. Installing supertest Package

Example: Install

npm i -D supertest

3. Creating Test Suites

Example: vitest describe/it

import { describe, it, expect, beforeAll, afterAll } from "vitest";

describe("GET /users", () => {
  beforeAll(async () => { /* seed db */ });
  afterAll(async  () => { /* cleanup */ });
  it("returns 200 + array", async () => { /* ... */ });
});

4. Writing Unit Tests

Example: Pure function test

import { parsePagination } from "../src/utils/pagination.js";
it("clamps limit to 100", () => {
  expect(parsePagination({ query: { limit: 5000 } })).toMatchObject({ limit: 100 });
});

5. Writing Integration Tests

Example: Full app + DB

import request from "supertest";
import { app } from "../src/app.js";

it("creates and lists a user", async () => {
  await request(app).post("/users").send({ email: "a@b.c" }).expect(201);
  const { body } = await request(app).get("/users").expect(200);
  expect(body.data).toHaveLength(1);
});

6. Using supertest for HTTP Requests

Example: Methods

await request(app).get("/users").expect(200);
await request(app).post("/users").send({ email: "x@y.z" }).expect(201);
await request(app).patch("/users/1").send({ name: "X" }).expect(200);
await request(app).delete("/users/1").expect(204);
Note: Pass app directly to request(app) — supertest binds an ephemeral port automatically. No need to app.listen().

7. Testing Route Handlers

Example: All verbs

describe("/users/:id", () => {
  it("GET 404 when missing", async () => {
    await request(app).get("/users/999").expect(404);
  });
  it("PUT 200 when valid", async () => {
    await request(app).put("/users/1").send({ email: "z@z.z" }).expect(200);
  });
});

8. Asserting Response Status

Example: Multiple assertions

const res = await request(app).get("/users/1");
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toMatch(/json/);

9. Asserting Response Body

Example: Match shape

const { body } = await request(app).get("/users/1").expect(200);
expect(body).toMatchObject({ id: 1, email: expect.stringContaining("@") });

10. Mocking Database Calls

Example: vi.mock

import { vi } from "vitest";
vi.mock("../src/db.js", () => ({
  db: { users: { findById: vi.fn().mockResolvedValue({ id: 1, email: "a@b.c" }) } }
}));

11. Setting Up Test Database

StrategyNotes
TestcontainersSpin Postgres in Docker per test run
SQLite in-memoryFast; not 1:1 with prod
Schema-per-workerParallel tests on shared Postgres
Truncate between testsIsolation; faster than re-create

12. Testing Middleware Functions

Example: Mount on tiny app

import express from "express";
import request from "supertest";
import { requireAuth } from "../src/middleware/auth.js";

it("rejects without token", async () => {
  const app = express();
  app.get("/x", requireAuth, (req, res) => res.json({ ok: true }));
  await request(app).get("/x").expect(401);
});

13. Testing Error Handlers

Example: Force error route

const app = express();
app.get("/boom", () => { throw new HttpError(418, "I'm a teapot"); });
app.use(errorHandler);

it("returns 418", async () => {
  const res = await request(app).get("/boom").expect(418);
  expect(res.body.error).toMatch(/teapot/i);
});