Org Skills

nodejs-testing

Design a test strategy for Node.js/TypeScript back-end services and write the tests — runner choice (Vitest, Jest, node:test), unit vs integration vs end-to-end balance, in-process HTTP tests with supertest or Fastify inject, real databases with Testcontainers, outbound HTTP mocking with MSW or nock, fake timers, database isolation, contract tests with Pact, factories, coverage and flaky async tests in CI.

Download .zip Raw Source
When agents use itUse this whenever the user asks how to test a Node API, route, controller, service, repository, queue worker or NestJS module; sets up vitest.config.ts, jest.config.ts or test scripts in package.json; asks about mocking modules, fetch, Prisma or time; hits "open handles", "Jest did not exit", timeouts or order-dependent failures; or asks "what should I test". Also apply it when reviewing Node.js test code or generating tests for back-end code the user just wrote, even if they don't say "test".

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill nodejs-testing -a github-copilot
# or with the org installer (adds .github/skills/nodejs-testing):
npx -y github:AGCO-Global/org-skills add skill nodejs-testing

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill nodejs-testing
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill nodejs-testing -g

Installs the backend-nodejs-skills plugin, which bundles all Backend / Node.js skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-nodejs-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill nodejs-testing -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill nodejs-testing -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill nodejs-testing -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill nodejs-testing -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

Node.js Testing

Back-end tests earn their keep by catching wiring, data and contract bugs, which mocks hide. Favour fast in-process tests that hit real HTTP routing and a real database, and mock only what crosses the network boundary you don't own.

1. Pick the runner

Runner Default when Notes
Vitest New TS/ESM projects Native ESM and TS, Jest-compatible API, fast watch; pool: 'forks' for native deps
Jest Existing Jest suites, NestJS default scaffolds ESM support still needs extra config; use ts-jest or @swc/jest
node:test Libraries or services wanting zero test deps Built-in runner, mock.fn, mock.timers, coverage via --experimental-test-coverage; smaller ecosystem

Don't migrate a healthy Jest suite for speed alone; do pick Vitest for new ESM services.

2. The pyramid for a service

Layer Share Tests what Dependencies
Static free Types (tsc --noEmit), lint (no-floating-promises) none
Unit ~30% Domain rules, mappers, validation schemas, pure use cases with fake ports in-memory fakes
Integration (in-process API) ~55% Route → validation → service → real DB; error mapping; auth guards Real Postgres/Redis via Testcontainers, outbound HTTP mocked
Contract ~5% Consumer/provider API agreements Pact broker or checked-in pacts
End-to-end ~10% A few critical journeys against a deployed build Full environment

The integration layer is where most confidence comes from: it exercises schemas, SQL and error handling together.

3. In-process HTTP tests

Export a buildApp(deps) from app.ts that does not call listen. Tests build the app with test config.

// Fastify: no socket, very fast
const app = await buildApp({ config: testConfig, db });
const res = await app.inject({ method: 'POST', url: '/orders', payload: { sku: 'A1', qty: 2 },
  headers: { authorization: `Bearer ${await tokenFor(user)}` } });
expect(res.statusCode).toBe(201);
expect(res.json()).toMatchObject({ sku: 'A1', qty: 2, status: 'pending' });
await app.close();
  • Express/Hono/NestJS: supertest(app) (Nest: Test.createTestingModule(...).compile(), then app.getHttpServer()); Hono also offers app.request().
  • Assert status, body shape and side effects (row in DB, message enqueued), plus the error cases: 400 on invalid input, 401/403, 404, 409.
  • Override dependencies through the composition root (Nest: .overrideProvider(PaymentGateway).useValue(fake)), not by patching modules.

4. Real dependencies vs mocks

Dependency Use Why
Your database Testcontainers (@testcontainers/postgresql) or a CI service container SQL, constraints, migrations and transactions are where bugs live
Redis, message broker Testcontainers Real semantics (TTL, acks)
Third-party HTTP APIs MSW (msw/node), nock (recent versions intercept fetch), or undici MockAgent Deterministic, no network
Clock, randomness, ids Injected ports or fake timers Determinism
Your own modules Don't mock; inject fakes via ports Module mocks couple tests to file layout
// global-setup.ts: one container per test run, migrations applied once
const pg = await new PostgreSqlContainer('postgres:16-alpine').start();
process.env.DATABASE_URL = pg.getConnectionUri();
await runMigrations(process.env.DATABASE_URL);

Set MSW (or nock) to fail on unhandled requests (server.listen({ onUnhandledRequest: 'error' }), nock.disableNetConnect()), so a forgotten stub fails loudly instead of calling production.

5. Database isolation

  • Transaction per test, rolled back in afterEach: fastest; requires the app to accept an injected transaction/connection. Not suitable when code under test opens its own transactions without savepoint support.
  • Truncate tables between tests (TRUNCATE ... RESTART IDENTITY CASCADE): simple, works with any code.
  • Schema or database per worker (test_${process.env.VITEST_POOL_ID}): enables parallel files without interference.
  • Never rely on test order or on data left by another test; each test creates what it needs through factories (buildOrder({ status: 'paid' }) with sensible defaults, e.g. @faker-js/faker with a fixed seed or fishery).

6. Mocks, time and async

  • Module mocks (vi.mock, jest.mock) only for true externals without an injection point (SDK singletons, node:fs in a CLI). They are hoisted: import mocked modules after declaring them, and reset with vi.restoreAllMocks() / restoreMocks: true.
  • Time: vi.useFakeTimers() + vi.setSystemTime(...), vi.advanceTimersByTimeAsync(...) for promise-based timers; node:test has mock.timers.enable(). Always restore real timers in afterEach; don't fake timers in tests that talk to a real DB driver.
  • Always await the thing under test; use await expect(p).rejects.toThrow(NotFoundError). A missing await makes tests pass while the assertion never runs.
  • Queue workers: call the job processor function directly for unit tests; for integration run the worker against a real Redis and await a completion event, never sleep.

7. Contract tests

When separate teams own consumer and provider, use Pact (@pact-foundation/pact): consumers generate pacts from their tests, providers verify them in CI against provider states, and a broker's can-i-deploy gates releases. For a single team with a shared monorepo, generated clients from OpenAPI plus schema validation in integration tests are usually enough.

8. Coverage, flakiness and CI

  • Coverage with the v8 provider (default in Vitest) or istanbul when you need exact branch mapping. Gate on changed-code coverage and critical modules; 80% on domain/application code is a sensible target, not a vanity global number.
  • CI: tsc --noEmit and lint → unit → integration (Docker available, sharded with --shard) → contract verify → e2e on deploy. Pin TZ=UTC, seed randomness, run with --sequence.shuffle occasionally to expose order coupling.
Flaky symptom Likely cause Fix
"Jest did not exit" / open handles Server, pool, Redis or timer left open await app.close(), close pools in afterAll, .unref() intervals; --detectOpenHandles to find it
Passes alone, fails in suite Shared DB rows or module state Per-test isolation, factories, reset mocks
Random timeouts Missing await, real network, container start in beforeEach Await everything, block network, start containers once globally
Fails only in CI TZ, locale, CPU slower, parallel DB access TZ=UTC, schema per worker, no wall-clock assertions

Deliverable format

When asked to write tests for a Node module or endpoint:

## Behaviours     list of cases: happy path, validation, auth, not found, conflict, dependency failure
## Setup          reused helpers (buildApp, factories, containers, MSW handlers); new ones added
## Tests          one `it` per behaviour, named as a sentence, full compiling TS
## Run            exact command and expected result; any Docker/env prerequisites

Anti-patterns to reject

  • Mocking the ORM/repository to test a handler, so no SQL is ever exercised.
  • SQLite or in-memory fakes standing in for Postgres/MySQL in integration tests.
  • Tests that listen on fixed ports or call real third-party APIs.
  • setTimeout/sleep to wait for async effects; unawaited assertions.
  • Shared seed data mutated across tests; order-dependent suites.
  • Snapshotting entire JSON responses including ids and timestamps.
  • Coverage targets gamed with assertion-free tests.