AgentSkillsCN

tdd-workflow

完整的测试驱动开发流程,遵循红-绿-重构循环

SKILL.md
--- frontmatter
name: tdd-workflow
description: Complete Test-Driven Development workflow with red-green-refactor cycle
license: MIT

TDD Workflow Skill

Use this skill when implementing features using Test-Driven Development methodology.

When to Use

  • Implementing new features or functions
  • Adding new API endpoints
  • Creating utility functions
  • Building components with complex logic
  • When high test coverage is required

TDD Cycle

code
┌──────────────────────────────────────────────────────────┐
│                                                          │
│   🔴 RED      →    🟢 GREEN    →    🔵 REFACTOR         │
│   Write test       Make pass        Improve code         │
│   that fails       (minimal)        (keep green)         │
│                                                          │
│                    ↺ Repeat                              │
└──────────────────────────────────────────────────────────┘

Step-by-Step Process

Phase 1: RED - Write Failing Test

  1. Understand the requirement

    • What is the input?
    • What is the expected output?
    • What are the edge cases?
  2. Write the simplest test case first

    typescript
    describe('calculateDiscount', () => {
      it('should return 0 for orders under $50', () => {
        expect(calculateDiscount(30)).toBe(0);
      });
    });
    
  3. Run the test - it MUST fail

    bash
    npm test -- calculateDiscount
    

Phase 2: GREEN - Make It Pass

  1. Write the minimal code to pass

    typescript
    function calculateDiscount(orderTotal: number): number {
      return 0;
    }
    
  2. Run the test - it MUST pass

    bash
    npm test -- calculateDiscount
    
  3. Don't over-engineer - Only write enough code to pass the current test

Phase 3: REFACTOR - Improve the Code

  1. Only refactor when tests are green
  2. Keep tests passing throughout
  3. Improve code quality:
    • Extract constants
    • Rename for clarity
    • Remove duplication
    • Simplify logic

Phase 4: Repeat

Add the next test case and repeat the cycle:

typescript
it('should return 10% for orders $50-$100', () => {
  expect(calculateDiscount(75)).toBe(7.5);
});

Test Structure

Arrange-Act-Assert Pattern

typescript
it('should apply discount to premium users', () => {
  // Arrange
  const user = createUser({ isPremium: true });
  const order = createOrder({ total: 100 });

  // Act
  const result = applyDiscount(user, order);

  // Assert
  expect(result.total).toBe(90);
  expect(result.discountApplied).toBe(10);
});

Test Naming Convention

typescript
// Format: should_[expected behavior]_when_[condition]
it('should return empty array when no items match filter', () => {});
it('should throw ValidationError when email is invalid', () => {});
it('should retry 3 times when API returns 503', () => {});

Complete Example

Requirement

Create a password validator that checks:

  • Minimum 8 characters
  • At least one uppercase letter
  • At least one number

TDD Implementation

typescript
// Step 1: RED - First test
describe('validatePassword', () => {
  it('should reject passwords shorter than 8 characters', () => {
    expect(validatePassword('Short1')).toEqual({
      valid: false,
      errors: ['Password must be at least 8 characters'],
    });
  });
});

// Step 2: GREEN - Minimal implementation
function validatePassword(password: string) {
  const errors: string[] = [];
  
  if (password.length < 8) {
    errors.push('Password must be at least 8 characters');
  }
  
  return { valid: errors.length === 0, errors };
}

// Step 3: RED - Next test
it('should reject passwords without uppercase', () => {
  expect(validatePassword('lowercase1')).toEqual({
    valid: false,
    errors: ['Password must contain an uppercase letter'],
  });
});

// Step 4: GREEN - Add uppercase check
function validatePassword(password: string) {
  const errors: string[] = [];
  
  if (password.length < 8) {
    errors.push('Password must be at least 8 characters');
  }
  if (!/[A-Z]/.test(password)) {
    errors.push('Password must contain an uppercase letter');
  }
  
  return { valid: errors.length === 0, errors };
}

// Step 5: RED - Test for number requirement
it('should reject passwords without numbers', () => {
  expect(validatePassword('NoNumbers')).toEqual({
    valid: false,
    errors: ['Password must contain a number'],
  });
});

// Step 6: GREEN - Add number check
function validatePassword(password: string) {
  const errors: string[] = [];
  
  if (password.length < 8) {
    errors.push('Password must be at least 8 characters');
  }
  if (!/[A-Z]/.test(password)) {
    errors.push('Password must contain an uppercase letter');
  }
  if (!/[0-9]/.test(password)) {
    errors.push('Password must contain a number');
  }
  
  return { valid: errors.length === 0, errors };
}

// Step 7: REFACTOR - Clean up
const PASSWORD_RULES = [
  { test: (p: string) => p.length >= 8, message: 'Password must be at least 8 characters' },
  { test: (p: string) => /[A-Z]/.test(p), message: 'Password must contain an uppercase letter' },
  { test: (p: string) => /[0-9]/.test(p), message: 'Password must contain a number' },
];

function validatePassword(password: string) {
  const errors = PASSWORD_RULES
    .filter(rule => !rule.test(password))
    .map(rule => rule.message);
  
  return { valid: errors.length === 0, errors };
}

// Step 8: RED - Happy path test
it('should accept valid passwords', () => {
  expect(validatePassword('ValidPass1')).toEqual({
    valid: true,
    errors: [],
  });
});
// Already passes! ✅

Best Practices

Do

  • Write one test at a time
  • Run tests after every change
  • Test behavior, not implementation
  • Keep tests independent
  • Use descriptive test names

Don't

  • Write tests after code
  • Skip the RED phase
  • Write multiple tests before implementing
  • Test private methods directly
  • Couple tests to implementation details

Commands

bash
# Run tests in watch mode
npm test -- --watch

# Run specific test file
npm test -- validatePassword

# Run with coverage
npm test -- --coverage

# Run and update snapshots
npm test -- --updateSnapshot

When TDD Might Not Fit

  • Exploratory/prototype code
  • UI layout (visual testing better)
  • Third-party integration spikes
  • Performance optimization (profile first)

In these cases, write tests after, but still write them.