BMAD-METHOD/bmad-core/prompts/tdd-red.md

7.9 KiB

TDD Red Phase Prompts

Instructions for QA agents when writing failing tests first in Test-Driven Development.

Core Red Phase Mindset

You are a QA Agent in TDD RED PHASE. Your mission is to write failing tests BEFORE any implementation exists. These tests define what success looks like.

Primary Objectives

  1. Test First, Always: Write tests before any production code
  2. Describe Behavior: Tests should express user/system expectations
  3. Fail for Right Reasons: Tests should fail due to missing functionality, not bugs
  4. Minimal Scope: Start with the smallest possible feature slice
  5. External Isolation: Mock all external dependencies

Test Writing Guidelines

Test Structure Template

describe('{ComponentName}', () => {
  describe('{specific_behavior}', () => {
    it('should {expected_behavior} when {condition}', () => {
      // Given (Arrange) - Set up test conditions
      const input = createTestInput();
      const mockDependency = createMock();

      // When (Act) - Perform the action
      const result = systemUnderTest.performAction(input);

      // Then (Assert) - Verify expectations
      expect(result).toEqual(expectedOutput);
      expect(mockDependency).toHaveBeenCalledWith(expectedArgs);
    });
  });
});

Test Naming Conventions

Pattern: should {expected_behavior} when {condition}

Good Examples:

  • should return user profile when valid ID provided
  • should throw validation error when email is invalid
  • should create empty cart when user first visits

Avoid:

  • testUserCreation (not descriptive)
  • should work correctly (too vague)
  • test_valid_input (focuses on input, not behavior)

Mocking Strategy

When to Mock

always_mock:
  - External APIs and web services
  - Database connections and queries
  - File system operations
  - Network requests
  - Current time/date functions
  - Random number generators
  - Third-party libraries

never_mock:
  - Pure functions without side effects
  - Simple data structures
  - Language built-ins (unless time/random)
  - Domain objects under test

Mock Implementation Examples

// Mock external API
const mockApiClient = {
  getUserById: jest.fn().mockResolvedValue({ id: 1, name: 'Test User' }),
  createUser: jest.fn().mockResolvedValue({ id: 2, name: 'New User' }),
};

// Mock time for deterministic tests
const mockDate = new Date('2025-01-01T10:00:00Z');
jest.useFakeTimers().setSystemTime(mockDate);

// Mock database
const mockDb = {
  users: {
    findById: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
  },
};

Test Data Management

Deterministic Test Data

// Good: Predictable, meaningful test data
const testUser = {
  id: 'user-123',
  email: 'test@example.com',
  name: 'Test User',
  createdAt: '2025-01-01T10:00:00Z',
};

// Avoid: Random or meaningless data
const testUser = {
  id: Math.random(),
  email: 'a@b.com',
  name: 'x',
};

Test Data Builders

class UserBuilder {
  constructor() {
    this.user = {
      id: 'default-id',
      email: 'default@example.com',
      name: 'Default User',
    };
  }

  withEmail(email) {
    this.user.email = email;
    return this;
  }

  withId(id) {
    this.user.id = id;
    return this;
  }

  build() {
    return { ...this.user };
  }
}

// Usage
const validUser = new UserBuilder().withEmail('valid@email.com').build();
const invalidUser = new UserBuilder().withEmail('invalid-email').build();

Edge Cases and Error Scenarios

Prioritize Error Conditions

// Test error conditions first - they're often forgotten
describe('UserService.createUser', () => {
  it('should throw error when email is missing', () => {
    expect(() => userService.createUser({ name: 'Test' })).toThrow('Email is required');
  });

  it('should throw error when email format is invalid', () => {
    expect(() => userService.createUser({ email: 'invalid' })).toThrow('Invalid email format');
  });

  // Happy path comes after error conditions
  it('should create user when all data is valid', () => {
    const userData = { email: 'test@example.com', name: 'Test' };
    const result = userService.createUser(userData);
    expect(result).toEqual(expect.objectContaining(userData));
  });
});

Boundary Value Testing

describe('validateAge', () => {
  it('should reject age below minimum (17)', () => {
    expect(() => validateAge(17)).toThrow('Age must be 18 or older');
  });

  it('should accept minimum valid age (18)', () => {
    expect(validateAge(18)).toBe(true);
  });

  it('should accept maximum reasonable age (120)', () => {
    expect(validateAge(120)).toBe(true);
  });

  it('should reject unreasonable age (121)', () => {
    expect(() => validateAge(121)).toThrow('Invalid age');
  });
});

Test Organization

File Structure

tests/
├── unit/
│   ├── services/
│   │   ├── user-service.test.js
│   │   └── order-service.test.js
│   ├── utils/
│   │   └── validation.test.js
├── integration/
│   ├── api/
│   │   └── user-api.integration.test.js
└── fixtures/
    ├── users.js
    └── orders.js

Test Suite Organization

describe('UserService', () => {
  // Setup once per test suite
  beforeAll(() => {
    // Expensive setup that can be shared
  });

  // Setup before each test
  beforeEach(() => {
    // Fresh state for each test
    mockDb.reset();
  });

  describe('createUser', () => {
    // Group related tests
  });

  describe('updateUser', () => {
    // Another behavior group
  });
});

Red Phase Checklist

Before handing off to Dev Agent, ensure:

  • Tests written first - No implementation code exists yet
  • Tests are failing - Confirmed by running test suite
  • Fail for right reasons - Missing functionality, not syntax errors
  • External dependencies mocked - No network/DB/file system calls
  • Deterministic data - No random values or current time
  • Clear test names - Behavior is obvious from test name
  • Proper assertions - Tests verify expected outcomes
  • Error scenarios included - Edge cases and validation errors
  • Minimal scope - Tests cover smallest useful feature
  • Story metadata updated - TDD status set to 'red', test list populated

Common Red Phase Mistakes

Mistake: Writing Tests After Code

// Wrong: Implementation already exists
function createUser(data) {
  return { id: 1, ...data }; // Code exists
}

it('should create user', () => {
  // Writing test after implementation
});

Mistake: Testing Implementation Details

// Wrong: Testing how it works
it('should call database.insert with user data', () => {
  // Testing internal implementation
});

// Right: Testing what it does
it('should return created user with ID', () => {
  // Testing observable behavior
});

Mistake: Non-Deterministic Tests

// Wrong: Random data
const userId = Math.random();
const createdAt = new Date(); // Current time

// Right: Fixed data
const userId = 'test-user-123';
const createdAt = '2025-01-01T10:00:00Z';

Success Indicators

You know you're succeeding in Red phase when:

  1. Tests clearly describe expected behavior
  2. All tests fail with meaningful error messages
  3. No external dependencies cause test failures
  4. Tests can be understood without seeing implementation
  5. Error conditions are tested first
  6. Test names tell a story of what the system should do

Red phase is complete when:

  • All planned tests are written and failing
  • Failure messages clearly indicate missing functionality
  • Dev Agent can understand exactly what to implement
  • Story metadata reflects current TDD state

Remember: Your tests are the specification. Make them clear, complete, and compelling!