TDD Industry Standard Guide — Full Stack R&D Reference
Scope: Node.js · NestJS · Express.js · React · Next.js · Angular · Python · Laravel
Audience: Engineering teams adopting or scaling TDD practices
Version: 2025 — reflects current ecosystem state
Part 1 — What Is TDD and How It Works
1.1 Core Philosophy
TDD (Test-Driven Development) is a development methodology where tests are written before the production code. It inverts the traditional flow — instead of writing code then checking if it works, you first define what "working" means.
Red → Green → Refactor cycle:
1. RED — Write a failing test for a behavior that doesn't exist yet
2. GREEN — Write the minimum code required to make that test pass
3. REFACTOR — Clean up the code without breaking the test
This isn't just about testing — it's a design discipline. Forcing yourself to write tests first exposes bad API design, tight coupling, and unclear responsibilities before they're baked into code.
1.2 TDD vs BDD vs ATDD
| Approach | Who writes | Language | Focus |
|---|---|---|---|
| TDD | Developer | Code/assertions | Unit behavior |
| BDD | Dev + QA | Gherkin (Given/When/Then) | User scenarios |
| ATDD | Dev + QA + Business | Acceptance criteria | System behavior |
In practice, most production teams use TDD for units + BDD for integration/E2E. Pure ATDD is rare outside regulated industries.
1.3 The Test Pyramid (Industry Standard Model)
/\
/ \
/ E2E \ ~10% — Slow, expensive, brittle
/--------\
/Integration\ ~20% — Services, DB, APIs
/--------------\
/ Unit Tests \ ~70% — Fast, isolated, numerous
/------------------\
Invert this ratio = test anti-pattern (too many E2E tests = slow CI, fragile suite).
Part 2 — Test Types and What to Cover
2.1 Unit Tests
- Test a single function, class, or module in isolation
- All dependencies are mocked/stubbed
- Should run in < 50ms per test
Cover:
- Pure functions (transformation, calculation, formatting)
- Service layer business logic
- Guards, interceptors, middlewares
- Validators and DTOs
- Utility helpers
2.2 Integration Tests
- Test two or more real components together
- May hit a real DB (test DB), real service layer, real HTTP handler
- Slower than unit tests but critical for wiring validation
Cover:
- Controller → Service → Repository chain
- Database queries with real schema
- Auth flows (token issuance + validation)
- Event-driven pipelines (queue → handler)
2.3 E2E Tests
- Test the full system from HTTP request to response
- Often uses a real or in-memory database
- Validates complete user workflows
Cover:
- Critical user journeys (login, checkout, form submission)
- API contract validation
- Cross-service flows in microservices
2.4 Contract Tests (Underused but Valuable)
- Tests API contracts between producer and consumer
- Tool: Pact.js (Node), Pactflow (SaaS)
- Essential in microservices architectures
2.5 Snapshot Tests
- Capture rendered output and detect unintended changes
- Best for UI components (React, Angular)
- Overuse leads to meaningless "approve all snapshots" habit
2.6 Test Case Identification Strategy
Before writing tests, apply these techniques:
Equivalence Partitioning — Group inputs into classes that behave the same
Boundary Value Analysis — Test at edges (0, -1, max, max+1)
Decision Table Testing — Matrix of conditions vs outcomes
State Transition — For stateful flows (e.g., order status machine)
For each function/module ask:
✓ Happy path — expected input → expected output
✓ Edge cases — empty, null, zero, max values
✓ Error paths — invalid input, network fail, DB error
✓ Security paths — injection, unauthorized access, rate limits
✓ Concurrency — race conditions, duplicate requests
Part 3 — Stack-Specific Setup
3.1 Node.js (Generic)
Packages:
{
"devDependencies": {
"jest": "^29.x",
"ts-jest": "^29.x",
"@types/jest": "^29.x",
"supertest": "^6.x",
"nock": "^13.x",
"faker": "^8.x"
}
}
jest.config.ts:
export default {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.ts', '**/*.test.ts'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.module.ts', '!src/main.ts']
}
3.2 NestJS (Primary Recommendation for Node APIs)
Packages (built-in + additions):
{
"devDependencies": {
"@nestjs/testing": "^10.x",
"jest": "^29.x",
"ts-jest": "^29.x",
"supertest": "^6.x",
"@testcontainers/postgresql": "^10.x"
}
}
Unit Test — Service (Red → Green example):
// users.service.spec.ts — RED phase
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<UserRepository>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UsersService,
{
provide: UserRepository,
useValue: {
findOne: jest.fn(),
save: jest.fn(),
}
}
]
}).compile();
service = module.get(UsersService);
repo = module.get(UserRepository);
});
it('should throw NotFoundException when user does not exist', async () => {
repo.findOne.mockResolvedValue(null);
await expect(service.findById('non-existent-id')).rejects.toThrow(NotFoundException);
});
});
Integration Test — Controller with real HTTP:
// users.e2e-spec.ts
describe('POST /users', () => {
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule]
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should create user and return 201', () => {
return request(app.getHttpServer())
.post('/users')
.send({ email: 'test@example.com', password: 'securePass123' })
.expect(201)
.expect(res => {
expect(res.body).toHaveProperty('id');
expect(res.body.email).toBe('test@example.com');
});
});
});
Testcontainers (Real DB in CI):
const container = await new PostgreSqlContainer('postgres:15').start();
// inject connection string into TypeORM config
3.3 Express.js
// Minimal setup, no DI framework — use jest + supertest
import request from 'supertest';
import app from '../app'; // your express app
describe('GET /health', () => {
it('returns 200 with status ok', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
});
});
Key difference from NestJS: No built-in DI, so use dependency injection manually or use a light container like awilix for testability.
3.4 React.js
Packages:
{
"devDependencies": {
"@testing-library/react": "^14.x",
"@testing-library/user-event": "^14.x",
"@testing-library/jest-dom": "^6.x",
"msw": "^2.x",
"vitest": "^1.x"
}
}
Vitest is now preferred over Jest for React/Vite projects (2024+)
// LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('should show error when email is invalid', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
await userEvent.type(screen.getByLabelText('Email'), 'notvalid');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
});
});
MSW for API Mocking (industry standard):
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users', () => HttpResponse.json([{ id: 1, name: 'Bhaumik' }]))
);
3.5 Next.js
Additional considerations:
- Test Server Components differently from Client Components
- Use
@testing-library/reactfor client components - Use Playwright or Cypress for full E2E (including SSR flows)
// app/actions/create-user.test.ts (Server Action testing)
import { createUser } from './create-user';
it('should return validation error for short password', async () => {
const result = await createUser({ email: 'a@b.com', password: '123' });
expect(result.error).toBe('Password too short');
});
3.6 Angular
Built-in: Jasmine + Karma (legacy) → now migrating to Jest + Vitest
// users.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UsersComponent],
providers: [provideHttpClientTesting()]
}).compileComponents();
fixture = TestBed.createComponent(UsersComponent);
component = fixture.componentInstance;
});
it('should display users from service', () => {
expect(component).toBeTruthy();
});
});
Angular 17+ recommendation: Use @analogjs/vitest-angular for faster testing.
3.7 Python
Packages:
pytest
pytest-cov
pytest-asyncio
pytest-mock
factory-boy
httpx (for async API tests)
# test_user_service.py
import pytest
from unittest.mock import AsyncMock
from app.services.user_service import UserService
@pytest.mark.asyncio
async def test_raises_not_found_when_user_missing():
repo = AsyncMock()
repo.find_by_id.return_value = None
service = UserService(repo)
with pytest.raises(UserNotFoundException):
await service.get_user("missing-id")
FastAPI integration test:
from httpx import AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_create_user_returns_201():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post("/users", json={"email": "a@b.com"})
assert response.status_code == 201
3.8 Laravel
Built-in: PHPUnit + Pest (recommended for modern projects)
// tests/Unit/UserServiceTest.php (Pest)
it('throws exception when user not found', function () {
$repo = Mockery::mock(UserRepository::class);
$repo->shouldReceive('findById')->andReturn(null);
$service = new UserService($repo);
expect(fn () => $service->getUser('abc'))->toThrow(UserNotFoundException::class);
});
// Feature test (Integration)
it('creates user via API', function () {
$response = $this->postJson('/api/users', [
'email' => 'test@example.com',
'password' => 'securePass123'
]);
$response->assertStatus(201)->assertJsonStructure(['id', 'email']);
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
});
Part 4 — TDD Flow in Practice
4.1 Standard TDD Workflow
Feature Requirement
│
▼
Identify Test Cases
(unit → integration → e2e)
│
▼
Write Failing Test ──────────────────────┐
│ │
▼ │
Run Test → FAIL (RED) ✓ │
│ │
▼ │
Write Minimum Code to Pass │
│ │
▼ │
Run Test → PASS (GREEN) ✓ │
│ │
▼ │
Refactor (extract, simplify, type) │
│ │
▼ │
Run All Tests → PASS ✓ │
│ │
▼ │
Next Test Case ──────────────────────────┘
│
▼
PR + CI enforces coverage gate
4.2 TDD for New Projects
1. Set up test infrastructure FIRST (jest, coverage config, CI pipeline)
2. Define interfaces/contracts before implementations
3. Write service interface → test against interface → implement
4. Bottom-up: Repository → Service → Controller → E2E
5. Enforce coverage thresholds from day 1 (80% min)
6. Commit convention: test commits separate from feature commits
4.3 TDD for Legacy Projects (Retroactive)
This is harder. Use the Strangler Fig Pattern:
1. Do NOT rewrite everything — add tests around existing code first
2. Identify critical paths → write characterization tests first
(these capture CURRENT behavior, not ideal behavior)
3. Refactor module by module, protected by characterization tests
4. Replace legacy code with TDD-developed replacements incrementally
5. Track coverage per module — set increasing baselines per sprint
Tool: Istanbul/c8 (JS), Coverage.py (Python), Xdebug (PHP)
Use --coverage-threshold flags per directory for gradual enforcement.
Part 5 — AI-Assisted TDD: The Wrapper System
This is your key architectural challenge: how do you ensure AI-generated code also meets your TDD standards?
5.1 The Problem
AI tools (Cursor, GitHub Copilot, Claude) generate code without context of your test standards. Developers may accept generated code without tests, leading to:
- Inconsistent test naming conventions
- Missing edge case coverage
- Wrong test granularity
- Tests that don't follow AAA (Arrange-Act-Assert) pattern
5.2 The Solution: TDD Standards Wrapper System
Layer 1 — Project-Level Rules File (.cursorrules or CLAUDE.md)
# TDD Contract — All Code Generated Must Follow These Rules
## Test Requirements
- Every function/method must have a corresponding .spec.ts file
- Tests follow AAA: Arrange → Act → Assert — labeled with comments
- Unit tests must mock ALL external dependencies
- No test should depend on another test's state
- Minimum one test per: happy path, error path, edge case
## Naming Convention
- Test file: [module].spec.ts (unit) / [module].e2e-spec.ts (integration)
- Describe block: class/function name
- It block: "should [expected behavior] when [condition]"
## Coverage Gate
- Lines: 80%, Branches: 75%, Functions: 80%
- New modules must not lower global coverage
## Test Structure Template
describe('[ModuleName]', () => {
// Setup
beforeEach(() => { /* setup */ });
afterEach(() => { /* cleanup */ });
describe('[methodName]', () => {
it('should [behavior] when [condition]', () => {
// Arrange
// Act
// Assert
});
});
});
Layer 2 — AI Prompt Wrapper (System Prompt / Custom Instructions)
When using Claude/Cursor for code generation, prefix with:
You are a senior engineer following strict TDD standards.
For every code change you make:
1. First output the test file (.spec.ts) using Jest + AAA pattern
2. Then output the implementation file
3. All external deps must be mocked in unit tests
4. Follow naming: "should [behavior] when [condition]"
5. Include: happy path, null/undefined edge case, error case
6. Do NOT write implementation before the test
Store this as:
-
/prompts/tdd-code-gen.md— for manual use - Cursor
.cursorrules— auto-injected to all AI interactions - VS Code snippet — for quick scaffold generation
Layer 3 — Pre-commit Hooks (Enforcement)
// package.json
{
"lint-staged": {
"src/**/*.ts": ["jest --findRelatedTests --passWithNoTests --coverage"]
},
"husky": {
"pre-commit": "lint-staged",
"pre-push": "jest --coverage --coverageThreshold='{\"global\":{\"lines\":80}}'"
}
}
Layer 4 — CI Pipeline Gate
# .github/workflows/test.yml
- name: Run Tests with Coverage
run: |
npm run test:cov
# Fail if coverage drops below threshold
npx jest --coverage --coverageThreshold='{"global":{"lines":80,"branches":75}}'
Layer 5 — ESLint Test Rules
// eslint plugin: eslint-plugin-jest
{
"plugins": ["jest"],
"rules": {
"jest/expect-expect": "error",
"jest/no-disabled-tests": "warn",
"jest/no-focused-tests": "error",
"jest/valid-expect": "error",
"jest/consistent-test-it": ["error", { "fn": "it" }]
}
}
Part 6 — Test Generation: Manual vs Automated
6.1 Manual Generation (AI-Assisted, Human-Reviewed)
Workflow:
- Developer writes interface/type → prompts AI for test scaffold
- AI generates test file using prompt wrapper (Layer 2)
- Developer reviews, adds domain-specific edge cases
- AI fills implementation given tests
Cursor workflow:
1. Create users.service.ts with method signatures only
2. Cmd+K: "Generate TDD tests for this service following .cursorrules"
3. Review + commit test file
4. Cmd+K: "Implement service methods to make these tests pass"
5. Run tests → iterate
6.2 Automated Generation Tools
| Tool | Language | Type | Notes |
|---|---|---|---|
| Copilot Test Agent | All | AI-gen | GitHub Copilot feature |
| CodiumAI / Qodo | JS/TS/Python | AI-gen unit | Best for auto unit test gen |
| diffblue Cover | Java | AI | Enterprise Java only |
| Pynguin | Python | Search-based | Academic, generates pytest |
| TestPilot | JS | AI | Microsoft Research tool |
| jest-auto-spies | TS | Helper | Auto-generates spy objects |
CodiumAI/Qodo is the current industry leader for automatic test generation in JS/TS/Python. It analyzes function signatures and generates multiple test cases automatically.
6.3 Scaffold Generation CLI (Custom Team Tool)
Build your own test scaffold generator:
// scripts/generate-test.ts
import { execSync } from 'child_process';
import * as fs from 'fs';
const moduleName = process.argv[2]; // e.g., "users"
const template = `
import { Test } from '@nestjs/testing';
import { ${toPascalCase(moduleName)}Service } from './${moduleName}.service';
describe('${toPascalCase(moduleName)}Service', () => {
let service: ${toPascalCase(moduleName)}Service;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [${toPascalCase(moduleName)}Service]
}).compile();
service = module.get(${toPascalCase(moduleName)}Service);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
`;
fs.writeFileSync(`src/${moduleName}/${moduleName}.service.spec.ts`, template);
console.log(`✓ Test scaffold created: ${moduleName}.service.spec.ts`);
// package.json
{
"scripts": {
"generate:test": "ts-node scripts/generate-test.ts"
}
}
Usage: npm run generate:test users
Part 7 — Team Alignment & Standards
7.1 Developer Onboarding Doc (1-pager)
Every new developer should receive this contract:
TDD Contract — [Team Name]
1. No PR is merged without tests
2. Tests are written BEFORE implementation (or alongside for legacy)
3. Test naming: "should [behavior] when [condition]"
4. Pyramid ratio: 70% unit / 20% integration / 10% E2E
5. Coverage gate: 80% enforced in CI — PRs that drop coverage are blocked
6. Mocks: use jest.fn() / jest.spyOn() for external deps
7. Test data: use factory functions, never hardcoded magic values
8. AI-generated code must include AI-generated tests (use .cursorrules)
9. Failing tests in main branch = P0 incident
10. Review tests as carefully as implementation — they ARE documentation
7.2 PR Review Checklist for Tests
□ Does every new function have at least one test?
□ Are failure/error paths tested?
□ Are external deps properly mocked?
□ Do tests follow AAA pattern?
□ Are test descriptions meaningful (not "it works")?
□ Is there test data factory used (no magic strings)?
□ Does coverage remain above threshold?
□ Are integration tests using real DB (testcontainers) or proper mocks?
7.3 Test Factory Pattern (Team Standard)
// test/factories/user.factory.ts
import { faker } from '@faker-js/faker';
export const createUserDto = (overrides = {}) => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
createdAt: new Date(),
...overrides
});
// Usage in tests:
const user = createUserDto({ email: 'specific@test.com' });
Part 8 — Package Ecosystem Summary
JavaScript / TypeScript
| Package | Purpose | Verdict |
|---|---|---|
| Jest | Test runner + assertions | Industry standard for Node/NestJS |
| Vitest | Vite-native test runner | Preferred for React/Next.js/Vite |
| Supertest | HTTP integration testing | Standard for Express/NestJS |
| MSW | API mock server (browser + Node) | Industry standard for React |
| Testcontainers | Real DB in Docker for tests | Best for integration tests |
| @faker-js/faker | Test data generation | Standard |
| jest-mock-extended | TypeScript-aware auto-mocking | Recommended for NestJS |
| Pact.js | Consumer-driven contract tests | Microservices only |
| Playwright | E2E browser testing | Now preferred over Cypress |
| Cypress | E2E browser testing | Still strong, but Playwright winning |
Python
| Package | Purpose |
|---|---|
| pytest | Standard test runner |
| pytest-cov | Coverage |
| pytest-asyncio | Async test support |
| factory-boy | Test data factories |
| respx / httpx | HTTP mocking for async |
| pytest-mock | Mock integration |
Laravel / PHP
| Package | Purpose |
|---|---|
| Pest | Modern test framework (recommended) |
| PHPUnit | Legacy but still supported |
| Mockery | Mocking |
| Laravel Dusk | Browser E2E |
Part 9 — What the Industry Is Doing Now
9.1 Current Trends (2024–2025)
AI-assisted test generation is mainstream now. GitHub Copilot, Cursor, and Qodo are being used to scaffold tests. The shift is: AI writes first drafts, engineers review and enforce standards.
Vitest is winning in frontend. For Vite-based projects (Vite + React, Next.js app router), Vitest is significantly faster than Jest due to native ESM support and Vite transform reuse.
Testcontainers adoption is rising. Instead of mocking DB entirely, teams spin up real PostgreSQL/Redis/MongoDB containers in CI via Testcontainers. Tests are more realistic but still isolated.
Contract testing in microservices is growing. Pact and Pactflow adoption is increasing as teams feel the pain of mismatched API contracts between services.
Playwright over Cypress for E2E. Playwright's multi-browser support, built-in parallelism, and better CI performance has made it the new default.
Test coverage as a PR gate, not a metric. Teams are moving away from tracking coverage as a KPI toward using it purely as a blocking threshold.
9.2 What Most Teams Actually Do
Reality check — not everyone does pure TDD:
| Approach | Prevalence | Notes |
|---|---|---|
| Test-after (write code then test) | ~50% of teams | Pragmatic, but riskier |
| TDD on critical paths only | ~30% of teams | Balanced approach |
| Pure TDD | ~10% of teams | Often fintech, healthcare |
| No tests | ~10% of teams | Startups pre-product-market-fit |
The pragmatic hybrid is most common: TDD for services/domain logic, integration tests for DB layer, Playwright for critical user flows.
Part 10 — Future Scope and Roadmap
10.1 Your Adoption Plan
Phase 1 — Foundation (Week 1–2)
- Configure Jest/Vitest per stack
- Set up coverage thresholds in CI
- Add pre-commit hooks (Husky + lint-staged)
- Create
.cursorruleswith TDD prompt wrapper - Create test factory templates per module
Phase 2 — Culture (Week 3–4)
- PR checklist enforcement
- Onboarding doc for new devs
- First "TDD session" — pair-program a feature end-to-end
- Retroactive coverage tracking for legacy modules
Phase 3 — Automation (Month 2)
- Build test scaffold generator CLI
- Evaluate Qodo/CodiumAI for auto test gen
- Integrate Testcontainers for integration tests
- Add contract tests for any inter-service APIs
Phase 4 — Maturity (Month 3+)
- Coverage reports in PR comments (via jest-coverage-comment action)
- Mutation testing with Stryker (tests your tests)
- Test quality metrics (flakiness tracking, test execution time)
10.2 Mutation Testing (Advanced)
Mutation testing checks if your tests are actually meaningful:
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
npx stryker run
Stryker introduces small bugs ("mutants") into your code and checks if your tests catch them. Mutation score > 70% means your tests have real value.
Appendix: Quick Reference
TDD Command Cheat Sheet
# NestJS
npm run test # unit tests
npm run test:e2e # integration/e2e tests
npm run test:cov # with coverage report
# React/Next.js (Vitest)
npx vitest # watch mode
npx vitest run --coverage # single run with coverage
# Python
pytest tests/ -v --cov=app --cov-report=html
# Laravel
php artisan test --coverage
./vendor/bin/pest --coverage
Coverage Report Integration (GitHub Actions)
- uses: ArtiomTr/jest-coverage-report-action@v2
with:
test-script: npm run test:cov
threshold: 80
Guide maintained by engineering team. Update when packages release major versions or architectural patterns shift.
Top comments (0)