§ unswayed-backend · The agent operating model
Testing & coverage
Testing & coverage
"Strict testing for everything" is a policy with teeth here. This page explains how it's enforced on the NestJS stack and why it's shaped this way.
TDD, then the pyramid
Work starts with a failing test (red → green → refactor). Tests are organized as a pyramid, all on Jest:
- Unit — services, pure logic, and edge cases, fast and isolated; colocated as
src/**/*.spec.ts. - Integration — a module's providers/controllers wired through the
@nestjs/testingDI container. - End-to-end — the real Nest app booted and driven over HTTP with
supertest (
test/*.e2e-spec.ts): status codes, response shapes, error bodies, auth, and stateful effects.
The scaffold ships one of each: a controller unit spec, a module DI smoke spec,
and an e2e spec that hits GET /.
The 90% coverage gate
package.json → jest.coverageThreshold.global enforces ≥ 90% on
statements, branches, functions, and lines (v8 provider). collectCoverageFrom
counts all of src except main.ts (bootstrap) and the spec files; the module
smoke test keeps modules counted. npm run verify (lint + typecheck + coverage +
e2e) fails below the gate — a hard build failure, not a dashboard you can ignore.
The one inviolable rule: never lower the threshold or disable a test to go green. Add the missing test or delete dead code. Changing the gate at all requires a recorded decision (an ADR).
Why SWC transforms the tests
This is the subtle part. With ts-jest, TypeScript's emitDecoratorMetadata
injects a conditional helper (a branch) into every decorated DI constructor —
code you can't meaningfully test. On a single controller that dragged branch
coverage down to ~75%, so a true 90% branch gate was unreachable without padding.
Switching the Jest transform to SWC (@swc/jest, configured in .swcrc with
legacyDecorator + decoratorMetadata) eliminates the phantom branch, so the
scaffold reports an honest 100%. The trade-off: SWC doesn't type-check, so
tsc runs as a separate npm run typecheck step inside verify. (See ADR-0006
in docs/DECISIONS.md.)
E2E never reads the developer's .env
E2E runs against .env.test, but for a long time the developer's real .env
could still leak into the run: @prisma/client (and the prisma CLI)
auto-load .env through a bundled dotenv the moment they're imported, injecting
live keys (CLOUDINARY_URL, OPENAI_API_KEY, RAPIDAPI_KEY, …) that flip the
storage / AI / external-jobs ports from their deterministic stubs to the real
adapters mid-suite. The first defence was a curated blanklist — but a list only
protects the keys someone remembered to add.
Since Phase 6 the defence is structural, in two layers (test/env-shelf.ts):
- The shelf. Jest's
globalSetuprenames.env→.env.e2e-shelvedbefore anything can read it;globalTeardownrenames it back, pass or fail. A shelf stranded by a hard-killed run self-heals on the next setup. With the file gone, every key — present and future — is protected with zero maintenance. (The production service reads.envonly at boot, so a running server doesn't notice a test run.) - The shell guard.
test/e2e-setup.tsstill blanks the integration keys per worker, because a developer may haveexport CLOUDINARY_URL=…in the shell environment — which no file shelving can remove. The keys are set to''(not deleted): dotenv never overrides an existing key, and everyconfiguredpredicate reads''as falsy → stub.
Proving the harness on day one
The scaffold's controller, service, and module are fully covered out of the box,
so npm run verify is green from the first commit (100% coverage, lint clean,
e2e passing) — the gate is real, not aspirational. The first real feature module
(nest g resource) extends the suite the same way: unit specs for the service,
an e2e spec for the route.