Testing Best Practices
Rules that keep Node API test suites fast, reliable, and focused on observable behavior.
How to Use This List
- Apply when adding the first test to a new service.
- Review during PRs that add mocks or integration infrastructure.
- Enforce via CI (required
npm test, separate integration job).
A - Test Design
- Test behavior, not private implementation. Assert HTTP status, returned JSON, and side effects - not internal call order.
- Follow the pyramid: many unit, fewer integration, rare E2E. Most tests complete in milliseconds.
- Name tests as specifications.
returns 409 when invoice already paidbeatstest1. - One primary assertion focus per test. Multiple asserts OK when verifying one behavior outcome.
- Table-driven cases for input variations. Cover edge cases without copy-paste tests.
B - HTTP & App Structure
- Export
createApp()without auto-listen. Supertest and Fastifyinjectneed in-process apps. - Cover error paths: 400, 401, 404, 409, 500 handlers. Happy-path-only suites miss regressions.
- Use Supertest or Fastify inject, not random ports. Avoid
EADDRINUSEand flaky CI. - Do not hit production URLs in tests. Staging or in-process only.
- Freeze time when testing TTL/expiry logic.
vi.setSystemTimeor injected clock interface.
C - Data & Isolation
- Isolate database per CI job or suite. Testcontainers or dedicated schema; never shared dev DB.
- Truncate or rollback between integration tests. Prevent order-dependent failures.
- Use factories/builders for test entities. Readable setup without 50-line fixtures inline.
- Seed minimal data per test. Large shared fixtures hide which field caused failure.
- Run migrations against test DB same as prod. Catch SQL differences early.
D - Tooling & CI
- Single
npm testentry; split unit vs integration when needed.test:integrationmay require Docker. - Prefer
node:testor Vitest for new code; plan Jest exit on brownfield. Avoid three runners. - Run tests on every PR after
typecheckandlint. Fail fast ordering in pipeline. - Do not merge flaky tests. Quarantine with owner ticket and expiry, or fix immediately.
- Contract tests for cross-service HTTP dependencies. Pact or schema diff before deploy.
E - Load & Release
- k6/Artillery smoke on staging before major launches. p95 and error rate thresholds defined.
- Load test write paths with cleanup strategy. Idempotent prefixes or dedicated tenant.
- Measure coverage; do not worship 100%. Critical domain modules targeted; generated code excluded.
- Test auth middleware with realistic tokens. Not
auth disabled in testglobally unless isolated unit. - Document how to run integration tests locally. Docker prerequisite in README.
FAQs
Why behavior over implementation?
Implementation tests break on refactor without behavior change. HTTP/domain assertions survive internal rewrites.
How isolated must the DB be?
Per CI job container is ideal. Minimum: separate database name and truncate between suites.
When are mocks appropriate?
External third-party APIs and slow infra in unit tests. Prefer real Postgres in integration for SQL correctness.
Should E2E tests live in service repo?
Critical journeys yes (deploy smoke). Full cross-product E2E may live in separate repo - keep service integration tests here.
How do I prevent flaky Supertest tests?
No shared mutable singletons; await all requests; reset modules or use fresh createApp() per test.
node:test or Vitest default?
node:test for pure logic zero-dep; Vitest when mocking/watch/coverage dominate workflow.
How many contract tests?
One pact per consumer-provider pair covering endpoints actually called - not full API surface.
Load test in every PR?
No - nightly or pre-release. Short smoke optional on release branches.
NestJS testing strategy?
Unit test services with mocked repos; e2e Supertest for controllers; Testcontainers for DB integration.
What about snapshot tests?
Sparingly for stable JSON error shapes; prefer explicit field assertions for APIs.
Related
- Testing Basics - pyramid introduction
- Supertest & HTTP Integration - in-process HTTP
- Testcontainers - real Postgres/Redis
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.