Node.js Test Strategy
Overview
When activated, deliver a structured, production-grade test strategy tailored to the project's stack (Node.js, Next.js, and/or NestJS). Cover all pyramid layers, tooling, coverage gates, CI/CD integration, and non-functional testing.
Step 1: Identify the Project Stack
Before generating anything, inspect the project to determine:
- Runtime: Node.js / Next.js (App Router or Pages) / NestJS
- Existing test tooling: check
package.jsonfor jest, vitest, cypress, playwright, supertest, etc. - Monorepo or single package: check for
turbo.json,nx.json,pnpm-workspace.yaml,lerna.json - Database/ORM: Prisma, TypeORM, Mongoose — affects integration test setup
- Auth layer: JWT, NextAuth, Passport — requires security-specific tests
Use this context to tailor every recommendation below.
Step 2: Test Pyramid — Layers and Tools
Layer 1 — Unit Tests (Jest)
Coverage target: 90%+ (lines, branches, functions)
Tools: jest, ts-jest or @swc/jest, @faker-js/faker, custom matchers
Key patterns:
- Test pure functions, services, utilities, guards, pipes, DTOs in isolation
- Mock all external dependencies (DB, HTTP, file system) with
jest.mock()or manual mocks - Use
describe/itblocks with AAA pattern (Arrange, Act, Assert) - Add custom matchers in
jest.setup.tsfor domain assertions (e.g.,toBeValidUUID,toMatchApiShape)
NestJS unit example:
// src/users/users.service.spec.ts
import { Test } from '@nestjs/testing';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';
describe('UsersService', () => {
let service: UsersService;
const mockRepo = { findOne: jest.fn(), save: jest.fn() };
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile();
service = module.get(UsersService);
});
it('should find a user by id', async () => {
mockRepo.findOne.mockResolvedValue({ id: '1', email: 'a@b.com' });
const user = await service.findById('1');
expect(user).toMatchObject({ email: 'a@b.com' });
});
});
Next.js unit example (React Testing Library):
// components/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('calls onClick when clicked', () => {
const handler = jest.fn();
render(<Button
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(handler).toHaveBeenCalledTimes(1);
});
});
Layer 2 — Integration Tests (Jest + Supertest)
Coverage target: 80%+ of API routes and DB interactions
Tools: supertest, @nestjs/testing, testcontainers or in-memory DB
Key patterns:
- Spin up the real NestJS app or Next.js API handler with a test database
- Test the full request/response cycle including middleware, guards, interceptors
- Use
beforeAllto seed data;afterAllto teardown - Prefer
testcontainers(real Postgres/Redis in Docker) for critical data paths
NestJS integration example:
// test/users.e2e-spec.ts (integration layer)
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { AppModule } from '../src/app.module';
import { INestApplication } from '@nestjs/common';
describe('Users API', () => {
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = module.createNestApplication();
await app.init();
});
afterAll(() => app.close());
it('GET /users/:id returns 200 for existing user', () => {
return request(app.getHttpServer())
.get('/users/1')
.expect(200)
.expect(res => expect(res.body).toHaveProperty('email'));
});
});
Contract testing (Pact) — for microservices:
// When services communicate over HTTP, add Pact consumer/provider tests
// Consumer defines the contract; provider verifies it in CI
Layer 3 — E2E Tests (Cypress or Playwright)
Coverage target: 100% of critical user paths
Tools: cypress (component + E2E) or playwright (cross-browser, recommended for Next.js)
Critical paths to always cover:
- Authentication flow (sign-up, login, logout, session expiry)
- Core business flow (checkout, form submission, dashboard load)
- Error states (404, 500, validation errors)
- Accessibility via
@axe-core/playwrightorcypress-axe
Playwright example:
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test('user can log in and see dashboard', async ({ page }) => {
await page.goto('/login');
await page.fill('[name=email]', 'user@test.com');
await page.fill('[name=password]', 'secret');
await page.click('[type=submit]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome');
});
Step 3: Coverage Configuration
Generate or update jest.config.ts with coverage thresholds:
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node', // use 'jsdom' for Next.js components
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/main.ts'],
coverageThresholds: {
global: {
lines: 90,
branches: 85,
functions: 90,
statements: 90,
},
},
coverageReporters: ['text', 'lcov', 'html'],
};
export default config;
Step 4: Performance Testing (k6 / Artillery)
Trigger: user mentions load testing, performance budgets, or SLA targets.
Tools: k6 (recommended) or artillery
k6 smoke + load script:
// tests/performance/load.k6.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 20 }, // ramp up
{ duration: '3m', target: 20 }, // sustained load
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95th percentile < 500ms
http_req_failed: ['rate<0.01'], // < 1% error rate
},
};
export default function () {
const res = http.get('https://staging.example.com/api/products');
check(res, { 'status 200': r => r.status === 200 });
sleep(1);
}
Run: k6 run tests/performance/load.k6.js
Step 5: Security Testing (OWASP ZAP)
Trigger: user asks about security tests, OWASP, API security, or pen testing.
Strategy:
- Use ZAP Baseline Scan in CI for passive scanning (no active attacks)
- Use ZAP Full Scan for staging environments only
- Supplement with
npm auditandsnykfor dependency scanning
CI step (GitHub Actions):
- name: OWASP ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.10.0
with:
target: 'https://staging.example.com'
rules_file_name: '.zap/rules.tsv'
fail_action: true
Step 6: Accessibility Testing (axe-core)
Trigger: user mentions a11y, WCAG, accessibility audits, or screen readers.
Tools: @axe-core/playwright, cypress-axe, jest-axe
Playwright a11y example:
// e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no critical a11y violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toHaveLength(0);
});
Step 7: CI/CD Quality Gates
Generate a .github/workflows/test.yml (or equivalent) with all gates:
name: Test & Quality Gates
on: [push, pull_request]
jobs:
unit-and-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm run test:ci # jest --coverage --ci
- name: Upload coverage
uses: codecov/codecov-action@v4
e2e:
runs-on: ubuntu-latest
needs: unit-and-integration
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:e2e
security:
runs-on: ubuntu-latest
needs: unit-and-integration
steps:
- run: npm audit --audit-level=high
performance:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: |
curl -L https://github.com/grafana/k6/releases/download/v0.52.0/k6-v0.52.0-linux-amd64.tar.gz | tar xz
./k6-v0.52.0-linux-amd64/k6 run tests/performance/load.k6.js
package.json test scripts:
{
"scripts": {
"test": "jest",
"test:ci": "jest --coverage --ci --runInBand",
"test:watch": "jest --watch",
"test:e2e": "playwright test",
"test:perf": "k6 run tests/performance/load.k6.js"
}
}
Step 8: Test Maintenance Guidelines
Enforce these rules when generating or reviewing tests:
- Fast: unit tests must complete in < 100ms each; use mocks aggressively
- Independent: no shared mutable state between tests; each test sets up its own data
- Deterministic: never use real timers (
Date.now(),setTimeout) — mock them withjest.useFakeTimers() - No false positives: avoid
expect(wrapper).toBeDefined()— assert on actual behavior - Custom matchers: add domain-specific matchers to
jest.setup.tsto reduce boilerplate and improve readability - Test naming:
it('should [expected behavior] when [condition]')pattern - No implementation details: for React, prefer
getByRole/getByLabelTextover test IDs; for services, assert outputs not internal calls
Output Format
When delivering a test strategy, structure the response as:
- Stack summary — what was detected
- Pyramid overview — which layers apply and why
- Ready-to-use code — paste-ready test files and configs
- Coverage targets table — per-layer goals
- CI/CD snippet — copy-paste workflow
- Next steps — prioritized list of what to implement first