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)