mcpskills.net
SkillsMCPsAgentsPrompts
mcpskills.net — A curated directory of AI agent Skills and MCP servers
TermsPrivacy
← Back to Skills
Development

Test-Driven Development

Drives development with tests. Use when implementing any logic, fixing any bug, or changing any behavior. Prove that code works.

by Addy OsmaniRepository →Source →

Write a failing test before writing the code that makes it pass. Tests are proof — "seems right" is not done.

When to Use

  • Implementing any new logic or behavior
  • Fixing any bug (the Prove-It Pattern)
  • Modifying existing functionality
  • Adding edge case handling

When NOT to use: Pure configuration changes, documentation updates, or static content changes.

The TDD Cycle

    RED                GREEN              REFACTOR
 Write a test    Write minimal code    Clean up the
 that fails  ──→  to make it pass  ──→  implementation

Step 1: RED — Write a Failing Test

// 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);
  });
});

Step 2: GREEN — Make It Pass

// 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;
}

Step 3: REFACTOR — Clean Up

With tests green, improve the code without changing behavior:

  • Extract shared logic
  • Improve naming
  • Remove duplication
  • Optimize if necessary

The Prove-It Pattern (Bug Fixes)

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

The Test Pyramid

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
 ╱──────────────────╲

Test Sizes

| 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 |

Writing Good Tests

Test State, Not Interactions

// 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')
  );
});

DAMP Over DRY in Tests

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');
});

Prefer Real Implementations Over Mocks

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

Use Arrange-Act-Assert Pattern

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);
});

Test Anti-Patterns to Avoid

| 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 |

Verification

After completing any implementation:

  • [ ] Every new behavior has a corresponding test
  • [ ] All tests pass: npm test
  • [ ] Bug fixes include a reproduction test
  • [ ] Test names describe the behavior being verified
  • [ ] No tests were skipped or disabled
  • [ ] Coverage hasn't decreased (if tracked)