Testing Strategy

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.

4 min read Level 2/5 #koa#production#testing
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.

RunnerImport styleWatch modeCoverage
node:testBuilt-in, zero install--watch flag--experimental-test-coverage
VitestESM-first, great DXFast 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 →