Test-Driven Development
Drives development with tests. Use when implementing any logic, fixing any bug, or changing any behavior. Prove that code works.
Drives development with tests. Use when implementing any logic, fixing any bug, or changing any behavior. Prove that code works.
Write a failing test before writing the code that makes it pass. Tests are proof — "seems right" is not done.
When NOT to use: Pure configuration changes, documentation updates, or static content changes.
RED GREEN REFACTOR
Write a test Write minimal code Clean up the
that fails ──→ to make it pass ──→ implementation
// RED: This test fails because createTask doesn't exist yet
describe('TaskService', () => {
it('creates a task with title and default status', async () => {
const task = await taskService.createTask({ title: 'Buy groceries' });
expect(task.id).toBeDefined();
expect(task.title).toBe('Buy groceries');
expect(task.status).toBe('pending');
expect(task.createdAt).toBeInstanceOf(Date);
});
});
// GREEN: Minimal implementation
export async function createTask(input: { title: string }): Promise<Task> {
const task = {
id: generateId(),
title: input.title,
status: 'pending' as const,
createdAt: new Date(),
};
await db.tasks.insert(task);
return task;
}
With tests green, improve the code without changing behavior:
When a bug is reported, do not start by trying to fix it. Start by writing a test that reproduces it.
// Step 1: Write the reproduction test (it should FAIL)
it('sets completedAt when task is completed', async () => {
const task = await taskService.createTask({ title: 'Test' });
const completed = await taskService.completeTask(task.id);
expect(completed.status).toBe('completed');
expect(completed.completedAt).toBeInstanceOf(Date); // This fails → bug confirmed
});
// Step 2: Fix the bug
export async function completeTask(id: string): Promise<Task> {
return db.tasks.update(id, {
status: 'completed',
completedAt: new Date(), // This was missing
});
}
// Step 3: Test passes → bug fixed, regression guarded
Invest testing effort according to the pyramid:
╱╲
╱ ╲ E2E Tests (~5%)
╱ ╲ Full user flows, real browser
╱──────╲
╱ ╲ Integration Tests (~15%)
╱ ╲ Component interactions, API boundaries
╱────────────╲
╱ ╲ Unit Tests (~80%)
╱ ╲ Pure logic, isolated, milliseconds each
╱──────────────────╲
| Size | Constraints | Speed | Example | |------|------------|-------|---------| | Small | Single process, no I/O, no network | Milliseconds | Pure function tests | | Medium | Multi-process OK, localhost only | Seconds | API tests with test DB | | Large | Multi-machine OK, external services | Minutes | E2E tests |
// Good: Tests what the function does (state-based)
it('returns tasks sorted by creation date', async () => {
const tasks = await listTasks({ sortBy: 'createdAt', sortOrder: 'desc' });
expect(tasks[0].createdAt.getTime())
.toBeGreaterThan(tasks[1].createdAt.getTime());
});
// Bad: Tests how the function works internally
it('calls db.query with ORDER BY created_at DESC', async () => {
await listTasks({ sortBy: 'createdAt', sortOrder: 'desc' });
expect(db.query).toHaveBeenCalledWith(
expect.stringContaining('ORDER BY created_at DESC')
);
});
In tests, DAMP (Descriptive And Meaningful Phrases) is better than DRY. Each test should tell a complete story.
// DAMP: Each test is self-contained and readable
it('rejects tasks with empty titles', () => {
const input = { title: '', assignee: 'user-1' };
expect(() => createTask(input)).toThrow('Title is required');
});
it('trims whitespace from titles', () => {
const input = { title: ' Buy groceries ', assignee: 'user-1' };
const task = createTask(input);
expect(task.title).toBe('Buy groceries');
});
Preference order (most to least preferred):
1. Real implementation → Highest confidence
2. Fake → In-memory version
3. Stub → Returns canned data
4. Mock (interaction) → Verifies method calls — use sparingly
it('marks overdue tasks when deadline has passed', () => {
// Arrange: Set up the test scenario
const task = createTask({
title: 'Test',
deadline: new Date('2025-01-01'),
});
// Act: Perform the action being tested
const result = checkOverdue(task, new Date('2025-01-02'));
// Assert: Verify the outcome
expect(result.isOverdue).toBe(true);
});
| Anti-Pattern | Problem | Fix | |---|---|---| | Testing implementation details | Tests break when refactoring | Test inputs and outputs | | Flaky tests | Erode trust in the suite | Use deterministic assertions | | Testing framework code | Wastes time | Only test YOUR code | | Snapshot abuse | Large snapshots nobody reviews | Use sparingly and review | | No test isolation | Tests fail together | Each test sets up its own state | | Mocking everything | Tests pass but production breaks | Prefer real implementations |
After completing any implementation:
npm test