Course outline

Express.js / Intermediate: modules, identity and adapters / Lesson 12

Testing policies, adapters and route contracts

Choose tests that prove the claimed boundary and avoid confusing mock response calls with a real HTTP integration.

About 4 min readContent reviewed
On this page

Lesson goals

  • Separate pure domain checks, handler tests and composed-stack tests.
  • Assert identities, effects, cleanup and public result shape with deterministic fixtures.

Test the boundary that could actually fail

A pure validator test proves local input decisions, not that express.json ran before the route. A handler test with response spies proves calls made to that fake, not actual header serialization or body suppression. A composed application test can verify routing and middleware; a transport test adds HTTP behavior. Choose the smallest meaningful test for each risk and state its limits. Keep service dependencies injectable so most policy fixtures run without a database. Do not start a project server or configured service merely to test a deterministic projection or identifier parser.

Derive cases from endpoint semantics

For a tenant course read, test visible, absent and denied records with the same ID, wrong query shape and a source failure. For a write, add invalid input with no write call, stale revision and uncertain response after commit. A cookie route needs expiry and logout cases, while a download needs missing-file and late-failure behavior. These cases should differ because the contracts differ. Avoid copying one generic status-only checklist to every endpoint: a 403 result does not prove the forbidden write never happened, and a 200 result does not prove the correct tenant’s record was returned.

Prove rejection occurs before a protected effect

This complete local test uses an injected service spy and a small policy adapter. An ineligible principal receives a denied result and the spy remains untouched. The allowed caller triggers exactly one synthetic lookup with the expected tenant. This is plain JavaScript, not an Express middleware test. Its value is the explicit invariant that policy precedes source access. An actual route test should preserve that invariant while additionally checking parser order, matching, error forwarding and the public response contract of the composed application.

javascript Complete offline policy-before-effect test
function readCard(principal,tenant,service) {
  if (principal.tenantId !== tenant) return {code:"DENIED"};
  return service.find(tenant);
}
const calls=[];
const service={find:tenant => {calls.push(tenant); return {id:"c1"};}};
console.assert(readCard({tenantId:"lab-b"},"lab-a",service).code === "DENIED");
console.assert(calls.length === 0);
console.assert(readCard({tenantId:"lab-a"},"lab-a",service).id === "c1");
console.assert(JSON.stringify(calls) === '["lab-a"]');

The denied call produces no source access; the allowed call records lab-a once. No framework or HTTP listener is involved.

Keep integration evidence as explicit artifacts

The JSON fixture names expected behavior for a real composed test without pretending that test ran. Each row should eventually map to assertions about both public output and relevant domain effects. Do not place real credentials or customer payloads in fixtures. Fix clocks and synthetic identities where timing or scope matters, and clean up any temporary resources owned by the test. This course parses fixture JSON and executes pure models only; installing an HTTP test package, opening a listener and exercising a configured source are deliberately outside its verification.

json Synthetic integration fixture manifest
{"route":"GET /tenants/:tenantId/courses/:courseId","cases":[{"name":"denied-tenant","expect":"safe-denial-and-no-source-call"},{"name":"visible-course","expect":"exact-public-card"},{"name":"source-failure","expect":"safe-error-and-cleanup"}],"execution":"not-performed-as-http-integration"}

The manifest records future assertions rather than completed HTTP checks. Its JSON syntax can be checked without a server.

Make a test report useful to the next reviewer

Record the exact runtime, executed model names, fixture counts and unexecuted integrations. A syntax check alone cannot prove routing, and a mocked send call cannot establish browser cookie enforcement. Repeat broader checks only when changes or unresolved risks justify them. When a failure appears, shrink it to a deterministic fixture before adjusting timeouts or adding retries. This keeps diagnosis tied to the broken contract. Use the project’s approved installed tools for future integration rather than downloading an unreviewed package simply to claim a larger test surface.

Common errors and fixes

A mock response test is called a full HTTP test

Why it happens
The test report ignored what the fake omits.
How to fix it
Label handler, composition and transport evidence separately.
Prevent it next time
State exact unexecuted boundaries beside results.

A denied response follows an unauthorized write

Why it happens
Assertions checked status but not side effects.
How to fix it
Assert the protected service was never called.
Prevent it next time
Use source spies for rejected write branches.

Timing tests fail unpredictably

Why it happens
Fixtures depended on wall-clock sleeps and live state.
How to fix it
Inject a fixed clock and deterministic sources.
Prevent it next time
Keep transport timing separate from local decision tests.

Check your understanding

1. Why assert record identity in addition to a 200 status?

Show worked solution

A successful status can accompany the wrong tenant’s record or an accidental source projection. Assert the exact eligible ID and permitted fields, then pair it with an ineligible principal using the same requested ID. This tests isolation and output shape rather than merely that some branch sent a response.

2. What should a report say when only pure functions and JSON were tested?

Show worked solution

Name those models, runtime and parsed fixtures, then explicitly state that Express matching, middleware, headers, listeners and source integrations were not exercised. That is useful narrow evidence. It becomes misleading only when described as a complete API or deployment check.

Key takeaways

  • Useful assertions cover identity, effects and public shape.
  • A test claim must match the boundary actually exercised.

Official references