HTTP Testing with Supertest

Unit tests skip the middleware stack, which is where most bugs live: a parser you forgot to mount, a route ordered after the catch-all, a header the error handler drops. Supertest 14,405 (npm 2,036 install --save-dev supertest) runs the real stack: hand it your app object and it calls app.listen(0) on an ephemeral port, issues a genuine request, and closes the server when the assertions finish.

test/books-api.test.js — the real stack, over real HTTPJavaScript
import { test, describe, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import request from 'supertest';
import { createApp } from '../src/app.js';
import { makeStore } from '../src/store.js';
describe('Bookshelf API', () => {
  let app;
  beforeEach(() => { app = createApp({ store: makeStore() }); });  // fresh data per test
  test('GET /healthz answers 200 with a JSON body', async () => {
    const res = await request(app).get('/healthz').expect('Content-Type', /json/).expect(200);
    assert.deepEqual(res.body, { status: 'ok' });
  });
  test('GET /api/books/:id returns 404 for a missing book', async () => {
    await request(app).get('/api/books/9999').expect(404, { error: { code: 'not_found' } });
  });
  test('POST /api/books creates and echoes a Location header', async () => {
    const res = await request(app).post('/api/books')
      .send({ title: 'Persuasion', author: 'Jane Austen' }).expect(201);
    assert.equal(res.headers.location, '/api/books/3');
    await request(app).get(res.headers.location).expect(200);      // follow it
  }); });
Output
  ✔ GET /healthz answers 200 with a JSON body (23.5511ms)
  ✔ GET /api/books/:id returns 404 for a missing book (5.3863ms)
  ✔ POST /api/books creates and echoes a Location header (22.6823ms)
ℹ tests 3   ℹ pass 3   ℹ fail 0

.expect() is overloaded: a number asserts the status, a string or regular expression after a header name asserts that header, and a second argument asserts the body by deep equality. .send(object) sets Content-Type: application/json. Add request(app).get('/nope').expect(404, { error: { code: 'no_route' } }) too: that catches a catch-all registered in the wrong place. Now comment out app.use(express.json()) and one test fails:

Output of 58
  ✖ POST /api/books creates and echoes a Location header (5.2751ms)
  Error: expected 201 "Created", got 422 "Unprocessable Entity"
      at TestContext.<anonymous> (file:///.../test/books-api.test.js:19:61)
ℹ tests 3   ℹ pass 2   ℹ fail 1

Without the parser req.body is undefined, the guard returns 422, and the store is never reached.