Skip to content

TDD Implementation Guide

Reza Bozorgi edited this page Jun 9, 2025 · 1 revision

TDD Implementation Guide

#AutoSDLC #TDD #Testing #Implementation

← Back to Index | ← Testing Strategy

Overview

This guide provides a comprehensive approach to Test-Driven Development (TDD) in the AutoSDLC system. Our TDD implementation is strict: tests are written first based on product specifications, verified to fail completely, and only then is implementation code written. No mocks are used - all tests work with real implementations.

TDD Principles in AutoSDLC

Core Philosophy

  1. Tests Are Specifications: Tests define the exact behavior expected
  2. Red-Green-Refactor: The only acceptable workflow
  3. No Mocks Policy: All tests use real implementations
  4. Coverage Is Mandatory: 100% coverage of product specifications
  5. Tests Drive Design: Implementation emerges from test requirements

The TDD Cycle

graph LR
    A[Product Spec] --> B[Write Tests]
    B --> C[Verify Red]
    C --> D{All Tests Fail?}
    D -->|No| E[Fix Tests]
    E --> C
    D -->|Yes| F[Implement Code]
    F --> G[Run Tests]
    G --> H{Tests Pass?}
    H -->|No| I[Fix Implementation]
    I --> G
    H -->|Yes| J[Refactor]
    J --> K[Run Tests]
    K --> L{Still Green?}
    L -->|No| M[Fix Refactoring]
    M --> K
    L -->|Yes| N[Complete]
Loading

Phase 1: Writing Tests First

From Specification to Tests

// Example: User Authentication Specification
interface AuthenticationSpec {
  title: "User Authentication System";
  requirements: [
    "Users can register with email and password",
    "Passwords must be at least 8 characters with mixed case, numbers, and symbols",
    "Users can login with valid credentials",
    "System generates JWT tokens on successful login",
    "Tokens expire after 24 hours",
    "Users can refresh tokens before expiry",
    "Failed login attempts are rate-limited"
  ];
  acceptanceCriteria: [
    "Registration fails with invalid email format",
    "Registration fails with weak passwords",
    "Login fails with incorrect credentials",
    "Login succeeds with correct credentials",
    "JWT tokens are valid and contain user information",
    "Expired tokens are rejected",
    "5 failed login attempts trigger 15-minute lockout"
  ];
}

// Transform specification into comprehensive tests
describe('User Authentication System', () => {
  let authService: AuthService;
  let database: Database;
  let tokenService: TokenService;
  
  beforeAll(async () => {
    // Setup real services - NO MOCKS
    database = new Database({
      connection: process.env.TEST_DATABASE_URL
    });
    await database.connect();
    
    tokenService = new TokenService({
      secret: process.env.TEST_JWT_SECRET
    });
    
    authService = new AuthService({
      database,
      tokenService
    });
  });
  
  afterAll(async () => {
    await database.disconnect();
  });
  
  beforeEach(async () => {
    // Clean database before each test
    await database.users.deleteAll();
    await database.loginAttempts.deleteAll();
  });
  
  describe('User Registration', () => {
    it('should allow registration with valid email and strong password', async () => {
      const result = await authService.register({
        email: 'user@example.com',
        password: 'StrongPass123!'
      });
      
      expect(result.success).toBe(true);
      expect(result.user).toBeDefined();
      expect(result.user.id).toBeDefined();
      expect(result.user.email).toBe('user@example.com');
      
      // Verify user exists in real database
      const dbUser = await database.users.findByEmail('user@example.com');
      expect(dbUser).toBeDefined();
      expect(dbUser.passwordHash).toBeDefined();
      expect(dbUser.passwordHash).not.toBe('StrongPass123!'); // Should be hashed
    });
    
    it('should reject registration with invalid email format', async () => {
      const invalidEmails = [
        'notanemail',
        'missing@tld',
        '@example.com',
        'user@',
        'user..name@example.com',
        'user name@example.com'
      ];
      
      for (const email of invalidEmails) {
        const result = await authService.register({
          email,
          password: 'ValidPass123!'
        });
        
        expect(result.success).toBe(false);
        expect(result.error).toContain('Invalid email format');
        
        // Verify no user created in database
        const dbUser = await database.users.findByEmail(email);
        expect(dbUser).toBeNull();
      }
    });
    
    it('should enforce password complexity requirements', async () => {
      const weakPasswords = [
        { password: 'short', error: 'at least 8 characters' },
        { password: 'alllowercase', error: 'uppercase letter' },
        { password: 'ALLUPPERCASE', error: 'lowercase letter' },
        { password: 'NoNumbers!', error: 'number' },
        { password: 'NoSymbols123', error: 'special character' },
        { password: '12345678', error: 'letters' }
      ];
      
      for (const { password, error } of weakPasswords) {
        const result = await authService.register({
          email: 'user@example.com',
          password
        });
        
        expect(result.success).toBe(false);
        expect(result.error).toContain(error);
      }
    });
    
    it('should prevent duplicate email registration', async () => {
      // First registration
      await authService.register({
        email: 'existing@example.com',
        password: 'FirstPass123!'
      });
      
      // Attempt duplicate
      const result = await authService.register({
        email: 'existing@example.com',
        password: 'SecondPass123!'
      });
      
      expect(result.success).toBe(false);
      expect(result.error).toContain('Email already registered');
    });
  });
  
  describe('User Login', () => {
    beforeEach(async () => {
      // Create test user
      await authService.register({
        email: 'test@example.com',
        password: 'TestPass123!'
      });
    });
    
    it('should allow login with correct credentials', async () => {
      const result = await authService.login({
        email: 'test@example.com',
        password: 'TestPass123!'
      });
      
      expect(result.success).toBe(true);
      expect(result.token).toBeDefined();
      expect(result.refreshToken).toBeDefined();
      expect(result.expiresIn).toBe(86400); // 24 hours
      
      // Verify token is valid JWT
      const decoded = tokenService.verify(result.token);
      expect(decoded.userId).toBeDefined();
      expect(decoded.email).toBe('test@example.com');
      expect(decoded.exp).toBeGreaterThan(Date.now() / 1000);
    });
    
    it('should reject login with incorrect password', async () => {
      const result = await authService.login({
        email: 'test@example.com',
        password: 'WrongPassword123!'
      });
      
      expect(result.success).toBe(false);
      expect(result.error).toBe('Invalid credentials');
      expect(result.token).toBeUndefined();
      
      // Verify login attempt recorded
      const attempts = await database.loginAttempts.findByEmail('test@example.com');
      expect(attempts.length).toBe(1);
      expect(attempts[0].successful).toBe(false);
    });
    
    it('should reject login with non-existent email', async () => {
      const result = await authService.login({
        email: 'nonexistent@example.com',
        password: 'AnyPass123!'
      });
      
      expect(result.success).toBe(false);
      expect(result.error).toBe('Invalid credentials');
    });
  });
  
  describe('Rate Limiting', () => {
    it('should lock account after 5 failed login attempts', async () => {
      // Register user
      await authService.register({
        email: 'ratelimit@example.com',
        password: 'CorrectPass123!'
      });
      
      // Make 5 failed attempts
      for (let i = 0; i < 5; i++) {
        await authService.login({
          email: 'ratelimit@example.com',
          password: 'WrongPass123!'
        });
      }
      
      // 6th attempt should be rate limited
      const result = await authService.login({
        email: 'ratelimit@example.com',
        password: 'CorrectPass123!' // Even with correct password
      });
      
      expect(result.success).toBe(false);
      expect(result.error).toContain('Too many failed attempts');
      expect(result.retryAfter).toBe(900); // 15 minutes in seconds
      
      // Verify lockout in database
      const user = await database.users.findByEmail('ratelimit@example.com');
      expect(user.lockedUntil).toBeDefined();
      expect(user.lockedUntil.getTime()).toBeGreaterThan(Date.now());
    });
    
    it('should reset failed attempts after successful login', async () => {
      await authService.register({
        email: 'reset@example.com',
        password: 'CorrectPass123!'
      });
      
      // Make 4 failed attempts
      for (let i = 0; i < 4; i++) {
        await authService.login({
          email: 'reset@example.com',
          password: 'WrongPass123!'
        });
      }
      
      // Successful login
      await authService.login({
        email: 'reset@example.com',
        password: 'CorrectPass123!'
      });
      
      // Failed attempts should be reset
      const attempts = await database.loginAttempts.countFailed('reset@example.com');
      expect(attempts).toBe(0);
    });
  });
  
  describe('Token Management', () => {
    it('should generate valid JWT tokens', async () => {
      await authService.register({
        email: 'token@example.com',
        password: 'TokenPass123!'
      });
      
      const loginResult = await authService.login({
        email: 'token@example.com',
        password: 'TokenPass123!'
      });
      
      // Manually verify token structure
      const parts = loginResult.token.split('.');
      expect(parts.length).toBe(3); // Header.Payload.Signature
      
      // Decode and verify payload
      const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
      expect(payload.email).toBe('token@example.com');
      expect(payload.iat).toBeDefined();
      expect(payload.exp).toBeDefined();
      expect(payload.exp - payload.iat).toBe(86400); // 24 hours
    });
    
    it('should reject expired tokens', async () => {
      // Create an expired token
      const expiredToken = tokenService.sign(
        { userId: '123', email: 'test@example.com' },
        { expiresIn: '-1h' } // Expired 1 hour ago
      );
      
      const result = await authService.validateToken(expiredToken);
      
      expect(result.valid).toBe(false);
      expect(result.error).toBe('Token expired');
    });
    
    it('should allow token refresh before expiry', async () => {
      await authService.register({
        email: 'refresh@example.com',
        password: 'RefreshPass123!'
      });
      
      const loginResult = await authService.login({
        email: 'refresh@example.com',
        password: 'RefreshPass123!'
      });
      
      // Wait a moment to ensure new token has different timestamp
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      const refreshResult = await authService.refreshToken(loginResult.refreshToken);
      
      expect(refreshResult.success).toBe(true);
      expect(refreshResult.token).toBeDefined();
      expect(refreshResult.token).not.toBe(loginResult.token);
      
      // Verify new token is valid
      const decoded = tokenService.verify(refreshResult.token);
      expect(decoded.email).toBe('refresh@example.com');
    });
  });
});

Phase 2: Verifying Red State

Verification Script

#!/bin/bash
# verify-all-tests-red.sh

echo "=== TDD Phase 1: Verifying all tests are RED ==="

# Run all tests without implementation
npm test -- --no-coverage --json > test-results.json

# Extract results
TOTAL=$(jq '.numTotalTests' test-results.json)
FAILED=$(jq '.numFailedTests' test-results.json)
PASSED=$(jq '.numPassedTests' test-results.json)

echo "Test Results:"
echo "  Total Tests: $TOTAL"
echo "  Failed Tests: $FAILED"
echo "  Passed Tests: $PASSED"

# Verify all tests fail
if [ "$PASSED" -ne 0 ]; then
    echo ""
    echo "ERROR: Some tests are passing without implementation!"
    echo "This violates TDD principles. The following tests passed:"
    jq '.testResults[].assertionResults[] | select(.status == "passed") | .title' test-results.json
    exit 1
fi

if [ "$FAILED" -ne "$TOTAL" ]; then
    echo ""
    echo "ERROR: Not all tests are failing!"
    echo "Expected $TOTAL failures, got $FAILED"
    exit 1
fi

echo ""
echo "✓ SUCCESS: All $TOTAL tests are failing (RED state confirmed)"
echo "✓ Ready to proceed with implementation"

# Generate coverage report of specifications
echo ""
echo "=== Specification Coverage Analysis ==="
node scripts/analyze-test-coverage.js

exit 0

Specification Coverage Analyzer

// scripts/analyze-test-coverage.js

const fs = require('fs');
const path = require('path');

class SpecificationCoverageAnalyzer {
  constructor(specFile, testFile) {
    this.specification = require(specFile);
    this.tests = this.parseTestFile(testFile);
  }
  
  parseTestFile(testFile) {
    const content = fs.readFileSync(testFile, 'utf-8');
    const tests = [];
    
    // Extract test descriptions
    const testMatches = content.matchAll(/it\(['"`](.+?)['"`]/g);
    for (const match of testMatches) {
      tests.push(match[1]);
    }
    
    return tests;
  }
  
  analyzeCoverage() {
    const coveredRequirements = new Set();
    const coveredCriteria = new Set();
    
    // Map tests to requirements
    this.tests.forEach(test => {
      this.specification.requirements.forEach((req, index) => {
        if (this.testCoversRequirement(test, req)) {
          coveredRequirements.add(index);
        }
      });
      
      this.specification.acceptanceCriteria.forEach((criteria, index) => {
        if (this.testCoversCriteria(test, criteria)) {
          coveredCriteria.add(index);
        }
      });
    });
    
    // Generate coverage report
    const requirementCoverage = (coveredRequirements.size / this.specification.requirements.length) * 100;
    const criteriaCoverage = (coveredCriteria.size / this.specification.acceptanceCriteria.length) * 100;
    
    console.log('\n=== Specification Coverage Report ===');
    console.log(`Requirements Coverage: ${requirementCoverage.toFixed(1)}%`);
    console.log(`Acceptance Criteria Coverage: ${criteriaCoverage.toFixed(1)}%`);
    
    // List uncovered items
    if (requirementCoverage < 100) {
      console.log('\nUncovered Requirements:');
      this.specification.requirements.forEach((req, index) => {
        if (!coveredRequirements.has(index)) {
          console.log(`  - ${req}`);
        }
      });
    }
    
    if (criteriaCoverage < 100) {
      console.log('\nUncovered Acceptance Criteria:');
      this.specification.acceptanceCriteria.forEach((criteria, index) => {
        if (!coveredCriteria.has(index)) {
          console.log(`  - ${criteria}`);
        }
      });
    }
    
    return {
      requirementCoverage,
      criteriaCoverage,
      totalCoverage: (requirementCoverage + criteriaCoverage) / 2
    };
  }
  
  testCoversRequirement(test, requirement) {
    // Implement logic to determine if a test covers a requirement
    const testLower = test.toLowerCase();
    const reqLower = requirement.toLowerCase();
    
    // Extract key words from requirement
    const keywords = reqLower.split(' ')
      .filter(word => word.length > 3)
      .filter(word => !['with', 'must', 'should', 'users', 'system'].includes(word));
    
    // Check if test description contains key words
    return keywords.some(keyword => testLower.includes(keyword));
  }
  
  testCoversCriteria(test, criteria) {
    // Similar logic for acceptance criteria
    return this.testCoversRequirement(test, criteria);
  }
}

// Run analyzer
const analyzer = new SpecificationCoverageAnalyzer(
  './specifications/auth-spec.json',
  './tests/auth.test.ts'
);

const coverage = analyzer.analyzeCoverage();

if (coverage.totalCoverage < 100) {
  console.error('\n❌ ERROR: Not all specifications are covered by tests!');
  process.exit(1);
}

console.log('\n✅ All specifications are covered by tests');

Phase 3: Implementation (Green Phase)

Implementation Guidelines

// Example: Implementing to pass tests
export class AuthService {
  constructor(
    private database: Database,
    private tokenService: TokenService,
    private cryptoService: CryptoService
  ) {}
  
  async register(credentials: RegisterCredentials): Promise<RegisterResult> {
    // Step 1: Validate email format
    if (!this.isValidEmail(credentials.email)) {
      return {
        success: false,
        error: 'Invalid email format'
      };
    }
    
    // Step 2: Validate password strength
    const passwordValidation = this.validatePassword(credentials.password);
    if (!passwordValidation.valid) {
      return {
        success: false,
        error: passwordValidation.error
      };
    }
    
    // Step 3: Check for existing user
    const existingUser = await this.database.users.findByEmail(credentials.email);
    if (existingUser) {
      return {
        success: false,
        error: 'Email already registered'
      };
    }
    
    // Step 4: Hash password and create user
    const passwordHash = await this.cryptoService.hashPassword(credentials.password);
    const user = await this.database.users.create({
      email: credentials.email,
      passwordHash,
      createdAt: new Date()
    });
    
    return {
      success: true,
      user: {
        id: user.id,
        email: user.email
      }
    };
  }
  
  private isValidEmail(email: string): boolean {
    // RFC 5322 compliant email regex
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    
    // Additional validations
    if (!emailRegex.test(email)) return false;
    if (email.includes('..')) return false;
    if (email.startsWith('.') || email.endsWith('.')) return false;
    
    const [localPart, domain] = email.split('@');
    if (localPart.length > 64) return false;
    if (domain.length > 255) return false;
    
    return true;
  }
  
  private validatePassword(password: string): PasswordValidation {
    const errors = [];
    
    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 (!/[a-z]/.test(password)) {
      errors.push('Password must contain a lowercase letter');
    }
    
    if (!/[0-9]/.test(password)) {
      errors.push('Password must contain a number');
    }
    
    if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
      errors.push('Password must contain a special character');
    }
    
    return {
      valid: errors.length === 0,
      error: errors.join(', ')
    };
  }
}

Progressive Implementation

#!/bin/bash
# progressive-implementation.sh

# Run tests continuously during implementation
npm test -- --watch --bail --verbose

# After each small change:
# 1. Save file
# 2. Check test output
# 3. Only proceed if making progress
# 4. If tests regress, undo last change

Phase 4: Refactoring (Maintaining Green)

Refactoring Checklist

// Before refactoring - ensure all tests pass
describe('Pre-refactoring verification', () => {
  it('should have all tests passing', async () => {
    const result = await runAllTests();
    expect(result.failedTests).toBe(0);
    expect(result.passedTests).toBe(result.totalTests);
  });
  
  it('should have adequate test coverage', async () => {
    const coverage = await getCoverage();
    expect(coverage.statements).toBeGreaterThan(80);
    expect(coverage.branches).toBeGreaterThan(80);
    expect(coverage.functions).toBeGreaterThan(80);
    expect(coverage.lines).toBeGreaterThan(80);
  });
});

// Refactoring improvements
class RefactoredAuthService {
  // Extract validation logic
  private validators = {
    email: new EmailValidator(),
    password: new PasswordValidator()
  };
  
  // Simplify main method
  async register(credentials: RegisterCredentials): Promise<RegisterResult> {
    // Validate inputs
    const validation = await this.validateRegistration(credentials);
    if (!validation.valid) {
      return { success: false, error: validation.error };
    }
    
    // Check uniqueness
    if (await this.emailExists(credentials.email)) {
      return { success: false, error: 'Email already registered' };
    }
    
    // Create user
    const user = await this.createUser(credentials);
    return { success: true, user };
  }
  
  // Extract complex logic into focused methods
  private async validateRegistration(credentials: RegisterCredentials): Promise<ValidationResult> {
    const emailValidation = this.validators.email.validate(credentials.email);
    if (!emailValidation.valid) {
      return emailValidation;
    }
    
    const passwordValidation = this.validators.password.validate(credentials.password);
    return passwordValidation;
  }
  
  private async emailExists(email: string): Promise<boolean> {
    const user = await this.database.users.findByEmail(email);
    return user !== null;
  }
  
  private async createUser(credentials: RegisterCredentials): Promise<User> {
    const passwordHash = await this.cryptoService.hashPassword(credentials.password);
    return await this.database.users.create({
      email: credentials.email,
      passwordHash,
      createdAt: new Date()
    });
  }
}

TDD Workflow Automation

Automated TDD Pipeline

# .github/workflows/tdd-enforcement.yml
name: TDD Enforcement

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  verify-tdd-compliance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Full history for commit analysis
          
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Verify test-first approach
        run: |
          # Check commit history
          ./scripts/verify-tdd-commits.sh ${{ github.event.pull_request.base.sha }}
          
      - name: Check test coverage
        run: |
          npm test -- --coverage
          ./scripts/verify-coverage.sh
          
      - name: Verify no mocks
        run: |
          ./scripts/check-for-mocks.sh
          
      - name: Generate TDD report
        run: |
          ./scripts/generate-tdd-report.sh > tdd-report.md
          
      - name: Comment on PR
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const report = fs.readFileSync('tdd-report.md', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: report
            });

TDD Metrics Collection

// metrics/TDDMetrics.ts
export class TDDMetricsCollector {
  async collectMetrics(projectPath: string): Promise<TDDMetrics> {
    return {
      testFirstCompliance: await this.calculateTestFirstCompliance(projectPath),
      testCoverage: await this.getTestCoverage(projectPath),
      mockUsage: await this.detectMockUsage(projectPath),
      redGreenRefactorCycles: await this.countTDDCycles(projectPath),
      averageTestSize: await this.calculateAverageTestSize(projectPath),
      testToCodeRatio: await this.calculateTestToCodeRatio(projectPath)
    };
  }
  
  private async calculateTestFirstCompliance(projectPath: string): Promise<number> {
    const commits = await this.getCommitHistory(projectPath);
    let testFirstCount = 0;
    let totalFeatures = 0;
    
    for (const feature of this.extractFeatures(commits)) {
      totalFeatures++;
      
      const testCommit = this.findFirstTestCommit(feature, commits);
      const implCommit = this.findFirstImplCommit(feature, commits);
      
      if (testCommit && implCommit && testCommit.date < implCommit.date) {
        testFirstCount++;
      }
    }
    
    return (testFirstCount / totalFeatures) * 100;
  }
  
  private async detectMockUsage(projectPath: string): Promise<MockUsage> {
    const testFiles = await this.findTestFiles(projectPath);
    const mockPatterns = [
      /jest\.mock/,
      /sinon\./,
      /td\.replace/,
      /proxyquire/,
      /mockery/,
      /@Mock/,
      /createMock/
    ];
    
    let filesWithMocks = 0;
    let totalMockCalls = 0;
    
    for (const file of testFiles) {
      const content = await fs.readFile(file, 'utf-8');
      let fileMockCount = 0;
      
      for (const pattern of mockPatterns) {
        const matches = content.match(pattern);
        if (matches) {
          fileMockCount += matches.length;
        }
      }
      
      if (fileMockCount > 0) {
        filesWithMocks++;
        totalMockCalls += fileMockCount;
      }
    }
    
    return {
      filesWithMocks,
      totalMockCalls,
      percentage: (filesWithMocks / testFiles.length) * 100
    };
  }
}

Agent Integration for TDD

PM Agent TDD Task Creation

// PM Agent: Creating TDD tasks
class PMAgent {
  async createTDDTask(requirement: Requirement): Promise<TDDTask> {
    // 1. Create test specification
    const testSpec = await this.generateTestSpecification(requirement);
    
    // 2. Create GitHub issue with test spec
    const issue = await this.github.createIssue({
      title: `TDD: ${requirement.title}`,
      body: this.formatTDDIssue(requirement, testSpec),
      labels: ['tdd', 'implementation', requirement.priority],
      assignees: ['coder-agent']
    });
    
    // 3. Create test file
    await this.createTestFile(testSpec, requirement);
    
    // 4. Verify tests fail
    const verificationResult = await this.verifyTestsFail(testSpec.filePath);
    if (!verificationResult.allFailing) {
      throw new Error('Some tests are not failing - TDD violation!');
    }
    
    // 5. Update Agent_Output.md
    await this.updateStatus({
      task: 'TDD Task Created',
      issue: issue.number,
      testFile: testSpec.filePath,
      status: 'Tests verified as RED',
      assignedTo: 'coder-agent'
    });
    
    return {
      issueNumber: issue.number,
      testSpecPath: testSpec.filePath,
      requirement: requirement,
      status: 'ready_for_implementation'
    };
  }
}

Coder Agent TDD Implementation

// Coder Agent: Implementing with TDD
class CoderAgent {
  async implementFeature(task: TDDTask): Promise<Implementation> {
    // Phase 1: Verify Red
    await this.verifyRedPhase(task.testSpecPath);
    
    // Phase 2: Implement Green
    const implementation = await this.implementGreenPhase(task);
    
    // Phase 3: Refactor
    const refactored = await this.refactorPhase(implementation);
    
    // Create PR
    const pr = await this.createPullRequest(refactored, task);
    
    return {
      pullRequest: pr,
      testResults: await this.getFinalTestResults(),
      coverage: await this.getCoverageReport()
    };
  }
  
  private async implementGreenPhase(task: TDDTask): Promise<Code> {
    const tests = await this.parseTests(task.testSpecPath);
    const implementation = new Implementation();
    
    for (const test of tests) {
      // Update status
      await this.updateStatus({
        phase: 'GREEN',
        currentTest: test.name,
        progress: this.calculateProgress(test, tests)
      });
      
      // Implement just enough to pass this test
      const code = await this.implementForTest(test);
      implementation.add(code);
      
      // Verify test passes
      const result = await this.runSingleTest(test);
      if (!result.passed) {
        throw new Error(`Failed to make test pass: ${test.name}`);
      }
    }
    
    return implementation;
  }
}

Best Practices

1. Test Quality

  • Write descriptive test names that explain the behavior
  • One assertion per test when possible
  • Test behavior, not implementation
  • Include edge cases and error scenarios
  • Use real data that reflects production scenarios

2. Implementation Discipline

  • Never write code without a failing test
  • Implement the simplest solution first
  • Refactor only when all tests are green
  • Keep test and implementation commits separate
  • Document TDD phases in commit messages

3. Continuous Integration

  • Automate red phase verification
  • Enforce coverage thresholds
  • Block merges without full test passes
  • Monitor TDD compliance metrics
  • Regular TDD process audits

4. Team Collaboration

  • Share test specifications before implementation
  • Review tests as thoroughly as implementation
  • Pair on complex test scenarios
  • Maintain shared testing utilities
  • Document testing patterns

Common Pitfalls and Solutions

Pitfall 1: Writing Tests After Code

Solution: Use commit hooks to verify test files are created first

#!/bin/bash
# .git/hooks/pre-commit
# Prevent implementation without tests

if git diff --cached --name-only | grep -E '\.(ts|js)
 | grep -v test; then
  if ! git diff --cached --name-only | grep -E '\.test\.(ts|js)
; then
    echo "Error: Implementation files detected without corresponding tests"
    echo "Please commit tests first (TDD)"
    exit 1
  fi
fi

Pitfall 2: Using Mocks

Solution: Provide test utilities for real implementations

// test-utils/real-implementations.ts
export class TestDatabase {
  private data = new Map();
  
  async connect() { /* Real connection logic */ }
  async insert(table: string, data: any) { /* Real insert */ }
  async find(table: string, query: any) { /* Real query */ }
  async cleanup() { /* Real cleanup */ }
}

export class TestEmailService {
  private sentEmails = [];
  
  async send(email: Email) {
    // Real email sending logic (to test inbox)
    this.sentEmails.push(email);
    return { id: generateId(), status: 'sent' };
  }
  
  getSentEmails() { return this.sentEmails; }
}

Pitfall 3: Incomplete Test Coverage

Solution: Specification-driven test generation

// Generate tests from specifications
class TestGenerator {
  generateFromSpec(spec: Specification): string {
    const tests = [];
    
    // Generate tests for each requirement
    for (const req of spec.requirements) {
      tests.push(this.generateRequirementTest(req));
    }
    
    // Generate tests for each acceptance criteria
    for (const criteria of spec.acceptanceCriteria) {
      tests.push(this.generateCriteriaTest(criteria));
    }
    
    // Generate edge case tests
    tests.push(...this.generateEdgeCaseTests(spec));
    
    return this.formatTestSuite(tests);
  }
}

Monitoring TDD Compliance

TDD Dashboard

interface TDDDashboard {
  compliance: {
    testFirstRate: number;      // % of features with tests written first
    mockFreeRate: number;       // % of tests without mocks
    coverageRate: number;       // Average test coverage
    redPhaseCompliance: number; // % following proper red phase
  };
  trends: {
    daily: TDDMetrics[];
    weekly: TDDMetrics[];
    monthly: TDDMetrics[];
  };
  violations: {
    recent: Violation[];
    byAgent: Map<string, Violation[]>;
    byType: Map<ViolationType, number>;
  };
}

Automated Reporting

#!/bin/bash
# generate-tdd-report.sh

echo "# TDD Compliance Report"
echo "Generated: $(date)"
echo ""

# Test-first compliance
echo "## Test-First Compliance"
git log --pretty=format:"%h %s" | ./scripts/analyze-tdd-commits.sh

# Mock usage
echo "## Mock Usage Analysis"
find . -name "*.test.ts" -o -name "*.test.js" | xargs grep -l "mock\|Mock\|stub" | wc -l

# Coverage trends
echo "## Coverage Trends"
npm test -- --coverage --json | jq '.coverageMap'

# Red phase violations
echo "## Red Phase Violations"
grep -r "TDD violation" logs/agent-*.log | tail -20

Conclusion

Test-Driven Development is not just a testing strategy in AutoSDLC—it's the fundamental methodology that ensures quality, maintainability, and correct implementation of requirements. By following these strict TDD principles:

  1. Always write tests first based on specifications
  2. Verify all tests fail before implementing
  3. Write minimal code to pass tests
  4. Never use mocks—work with real implementations
  5. Refactor only when tests are green

The AutoSDLC system ensures that every feature is built correctly from the ground up, with comprehensive test coverage and high confidence in the implementation.

Related Documents


Tags: #AutoSDLC #TDD #Testing #Implementation #BestPractices Last Updated: 2025-06-09


Clone this wiki locally