Test Middleware in Isolation Before You Wire Up Routes
Testing Strategy
Good Koa test suites test middleware units in isolation, then use integration tests via app.callback() to verify the full request pipeline.
What you'll learn
- Choose between node:test and Vitest for a Koa project
- Test a single middleware function by constructing a mock ctx object
- Understand the three layers of a Koa test pyramid
Koa’s thin layer of abstraction makes it easy to test both individual middleware functions and full HTTP routes. A healthy test suite usually has three layers: unit tests for middleware logic, integration tests for route composition, and end-to-end tests for critical paths in a real environment.
Choosing a Test Runner
Two runners pair naturally with a modern Koa ESM project.
| Runner | Import style | Watch mode | Coverage |
|---|---|---|---|
node:test | Built-in, zero install | --watch flag | --experimental-test-coverage |
| Vitest | ESM-first, great DX | Fast HMR-style | @vitest/coverage-v8 |
Both work well. This chapter uses Vitest for familiarity, but every example is
trivially portable to node:test.
Unit-Testing a Middleware
Because middleware is just an async function(ctx, next), you can test it
without starting an HTTP server. Build a minimal ctx stub and pass a next
stub alongside it.
// middleware/requireJson.js
export async function requireJson(ctx, next) {
if (!ctx.is("application/json")) {
ctx.throw(415, "JSON required");
}
await next();
} // middleware/requireJson.test.js
import { describe, it, expect, vi } from "vitest";
import { requireJson } from "./requireJson.js";
function makeCtx(contentType) {
return {
is: (type) => contentType === type,
throw: (status, msg) => { const e = new Error(msg); e.status = status; throw e; },
};
}
describe("requireJson", () => {
it("calls next when content-type is JSON", async () => {
const ctx = makeCtx("application/json");
const next = vi.fn().mockResolvedValue(undefined);
await requireJson(ctx, next);
expect(next).toHaveBeenCalledOnce();
});
it("throws 415 for non-JSON requests", async () => {
const ctx = makeCtx("text/plain");
await expect(requireJson(ctx, vi.fn())).rejects.toMatchObject({ status: 415 });
});
}); Integration Tests
For route-level tests, compose the real app and pass app.callback() to
Supertest (covered in the next lesson). This catches wiring bugs — missing
await next() calls, incorrect middleware order — that unit tests cannot
surface.
The Test Pyramid in Practice
Keep the pyramid healthy:
- Many fast middleware unit tests — no network, no I/O.
- Some integration tests per route group — verify the composed pipeline.
- Few end-to-end tests — smoke-test critical user paths against a running server.
This structure keeps the suite fast while giving you confidence in the full stack.
Up Next
Learn how to fire real HTTP requests against your Koa app without binding a port.
HTTP Testing with Supertest →