← Documentation Home

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.

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

GREEN Phase

REFACTOR Phase


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

Test Reliability


TDD Enforcement

See TDD Enforcement for automated compliance checking.


Resources