TDD Methodology
TDD Methodology
Test-Driven Development (TDD) is a fundamental development practice enforced across all LLM platform projects. TDD ensures code quality, maintainability, and reliability through a disciplined RED-GREEN-REFACTOR cycle.
Core Principles
1. Tests First, Code Second
NEVER write implementation code before tests exist.
- Write failing tests that define expected behavior
- Implement minimal code to make tests pass
- Refactor with confidence knowing tests protect against regressions
2. RED-GREEN-REFACTOR Cycle
The foundational workflow of TDD:
graph LR A[RED: Write Failing Test] --> B[GREEN: Make Test Pass] B --> C[REFACTOR: Improve Code] C --> A
RED Phase
- Write a test for the next bit of functionality
- Test MUST fail (proves test is valid)
- Failure should be for the right reason (not syntax errors)
GREEN Phase
- Write minimal code to make test pass
- Don't worry about perfect code yet
- Focus solely on passing the test
REFACTOR Phase
- Clean up code while keeping tests green
- Improve design, remove duplication
- Optimize performance
- Tests provide safety net
TDD Workflow Example
Step 1: RED - Write Failing Test
// tests/auth/login.test.ts import { describe, it, expect } from 'vitest' import { AuthService } from '../src/auth/AuthService' describe('AuthService', () => { it('should authenticate user with valid credentials', async () => { const auth = new AuthService() const result = await auth.login('user@example.com', 'password123') expect(result.success).toBe(true) expect(result.user).toBeDefined() expect(result.token).toBeDefined() }) })
Run test: FAILS (AuthService doesn't exist)
Step 2: GREEN - Make Test Pass
// src/auth/AuthService.ts export class AuthService { async login(email: string, password: string) { // Minimal implementation to pass test return { success: true, user: { email }, token: 'mock-token' } } }
Run test: PASSES
Step 3: REFACTOR - Improve Code
// src/auth/AuthService.ts import bcrypt from 'bcrypt' import jwt from 'jsonwebtoken' export class AuthService { constructor(private userRepository: UserRepository) {} async login(email: string, password: string): Promise<LoginResult> { const user = await this.userRepository.findByEmail(email) if (!user) { throw new Error('Invalid credentials') } const isValid = await bcrypt.compare(password, user.passwordHash) if (!isValid) { throw new Error('Invalid credentials') } const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!) return { success: true, user: { id: user.id, email: user.email }, token } } }
Add more tests for edge cases, then refactor again.
TDD Best Practices
Write Tests at Multiple Levels
// Unit Test - Fast, isolated describe('calculateTotal', () => { it('should sum array of numbers', () => { expect(calculateTotal([1, 2, 3])).toBe(6) }) }) // Integration Test - Multiple components describe('Order API', () => { it('should create order and update inventory', async () => { const order = await createOrder({ items: [...] }) const inventory = await getInventory(order.items[0].productId) expect(inventory.quantity).toBe(originalQuantity - 1) }) }) // E2E Test - Full user workflow describe('Checkout Flow', () => { it('should complete purchase from cart to confirmation', async () => { await page.goto('/cart') await page.click('[data-testid="checkout-button"]') // ... complete flow }) })
Test Names Should Be Descriptive
// BAD it('works', () => { ... }) it('test1', () => { ... }) // GOOD it('should throw error when email is invalid', () => { ... }) it('should return 404 when user not found', () => { ... }) it('should cache results for 5 minutes', () => { ... })
One Assertion Per Test (When Possible)
// BAD - Multiple unrelated assertions it('should handle user operations', () => { expect(createUser(...)).toBeDefined() expect(deleteUser(...)).toBe(true) expect(updateUser(...)).toEqual(...) }) // GOOD - Focused tests describe('User operations', () => { it('should create user successfully', () => { expect(createUser(...)).toBeDefined() }) it('should delete user successfully', () => { expect(deleteUser(...)).toBe(true) }) it('should update user successfully', () => { expect(updateUser(...)).toEqual(...) }) })
Test Edge Cases and Error Conditions
describe('divide', () => { it('should divide two positive numbers', () => { expect(divide(10, 2)).toBe(5) }) it('should handle negative numbers', () => { expect(divide(-10, 2)).toBe(-5) }) it('should throw error when dividing by zero', () => { expect(() => divide(10, 0)).toThrow('Division by zero') }) it('should handle decimal results', () => { expect(divide(10, 3)).toBeCloseTo(3.333, 3) }) })
TDD Anti-Patterns to Avoid
Writing Tests After Implementation
Problem: Tests become validation of existing code rather than specification of desired behavior.
Solution: Always write tests first.
Testing Implementation Details
// BAD - Tests internal implementation it('should call validateEmail method', () => { const spy = jest.spyOn(service, 'validateEmail') service.register('user@example.com') expect(spy).toHaveBeenCalled() }) // GOOD - Tests behavior it('should reject invalid email addresses', () => { expect(() => service.register('invalid-email')) .toThrow('Invalid email format') })
Large Test Setup
// BAD - Massive setup it('should process order', () => { const user = createUser(...) const account = createAccount(...) const product1 = createProduct(...) const product2 = createProduct(...) const cart = createCart(...) cart.addItem(product1) cart.addItem(product2) // ... 20 more lines of setup expect(processOrder(cart)).toBe(true) }) // GOOD - Use factories/fixtures it('should process order', () => { const cart = createTestCart({ itemCount: 2 }) expect(processOrder(cart)).toBe(true) })
Slow Tests
Problem: Slow tests discourage running them frequently.
Solution:
- Mock external dependencies
- Use in-memory databases for integration tests
- Parallelize test execution
- Keep unit tests fast (<10ms each)
TDD in Different Contexts
API Development
// 1. Define OpenAPI spec first // openapi/auth.yaml paths: /auth/login: post: requestBody: required: true content: application/json: schema: type: object properties: email: { type: string } password: { type: string } // 2. Write contract test it('should match OpenAPI specification', async () => { const response = await request(app) .post('/auth/login') .send({ email: 'user@example.com', password: 'pass' }) expect(response.status).toBe(200) expect(response.body).toMatchSchema(loginResponseSchema) }) // 3. Implement endpoint // 4. Refactor
Drupal Module Development
// 1. Write test first class MyModuleTest extends KernelTestBase { public function testCustomEntityCreation() { $entity = MyCustomEntity::create([ 'title' => 'Test Entity', 'status' => 1, ]); $entity->save(); $this->assertNotNull($entity->id()); $this->assertEquals('Test Entity', $entity->getTitle()); } } // 2. Implement entity // 3. Run tests: phpunit // 4. Refactor
LLM Agent Development
// 1. Write test for agent behavior it('should generate code based on specification', async () => { const agent = new CodeGeneratorAgent() const spec = loadOpenAPISpec('user-api.yaml') const result = await agent.generate({ spec }) expect(result.files).toContain('src/api/user.ts') expect(result.testsGenerated).toBe(true) expect(result.coverage).toBeGreaterThan(0.8) }) // 2. Implement agent // 3. Run tests // 4. Refactor
TDD Metrics
Coverage Requirements
Minimum 80% code coverage across all projects:
# Run tests with coverage npm run test:coverage # Check coverage report # Statements: 85% # Branches: 82% # Functions: 88% # Lines: 84%
Test Execution Time Targets
- Unit Tests: <10ms per test
- Integration Tests: <100ms per test
- E2E Tests: <5s per test
- Full Suite: <5 minutes
Test Reliability
- Flaky Tests: 0% tolerance
- False Positives: Immediate investigation
- False Negatives: Immediate investigation
TDD Enforcement
See TDD Enforcement for automated compliance checking.
Resources
- Jest Documentation: https://jestjs.io/
- Vitest Documentation: https://vitest.dev/
- PHPUnit Documentation: https://phpunit.de/
- Playwright E2E: https://playwright.dev/
- Kent Beck's TDD Book: Test-Driven Development by Example