Advanced

This tutorial covers advanced TDD patterns for enterprise environments including legacy code testing, approval testing, TDD in distributed systems, microservices patterns, and scaling TDD across teams.

Example 59: Characterization Tests for Legacy Code

Characterization tests capture current behavior of legacy code without refactoring. They document what the system actually does (not what it should do) to establish a safety net before changes.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
    A[Legacy Code]
    B[Characterization Tests]
    C[Refactoring]
    D[Verified Code]

    A -->|Capture behavior| B
    B -->|Safety net created| C
    C -->|Tests pass| D

    style A fill:#DE8F05,stroke:#000,color:#fff
    style B fill:#0173B2,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#CC78BC,stroke:#000,color:#fff

Red: Test unknown legacy behavior

test("characterize calculation logic", () => {
  // => FAILS: Don't know what legacyCalculate returns
  const result = legacyCalculate(100, 10, "SPECIAL");
  expect(result).toBe(???); // Unknown expected value
});

Green: Run and capture actual behavior

function legacyCalculate(amount: number, rate: number, type: string): number {
  // => Complex legacy logic
  let result = amount * rate; // => Multiply amount by rate
  if (type === "SPECIAL") {
    // => Branch for SPECIAL type
    result = result * 1.5 + 25; // => Apply mysterious formula
  } // => result after SPECIAL formula
  return result; // => Return calculated value
}

describe("legacyCalculate characterization", () => {
  test("characterizes normal calculation", () => {
    const result = legacyCalculate(100, 10, "NORMAL");
    expect(result).toBe(1000); // => Captured actual behavior
  }); // => Documents what system does now

  test("characterizes SPECIAL type calculation", () => {
    const result = legacyCalculate(100, 10, "SPECIAL");
    expect(result).toBe(1525); // => Captured: (100*10)*1.5 + 25 = 1525
  }); // => Mysterious formula now documented

  test("characterizes edge case", () => {
    const result = legacyCalculate(0, 5, "SPECIAL");
    expect(result).toBe(25); // => Captured: (0*5)*1.5 + 25 = 25
  }); // => Documents zero case behavior
});

Key Takeaway: Characterization tests capture current behavior (even if buggy) to create safety net before refactoring. Write tests that pass with existing code, then refactor with confidence.

Why It Matters: Legacy code without tests is too risky to change. Michael Feathers’ “Working Effectively with Legacy Code” shows characterization tests enable safe refactoring - Characterization testing can significantly reduce legacy code incidents for critical systems.

Example 60: Approval Testing for Complex Outputs

Approval testing (golden master testing) compares large outputs against approved baseline. Ideal for testing complex data structures, reports, or generated code.

Red: Test complex report generation

test("generates user report", () => {
  // => FAILS: Complex report structure
  const report = generateReport(users);
  // How to assert on 50+ line report?
});

Green: Approval testing implementation

import { verify } from "approvals";

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

function generateReport(users: User[]): string {
  // => Generates multi-line report
  let report = "USER REPORT\n"; // => Header
  report += "===========\n";

  users.forEach((user) => {
    // => For each user
    report += `ID: ${user.id}\n`; // => Add ID line
    report += `Name: ${user.name}\n`; // => Add name line
    report += `Email: ${user.email}\n`; // => Add email line
    report += `Role: ${user.role}\n`; // => Add role line
    report += "---\n"; // => Separator
  });

  return report; // => Return formatted report
}

test("approval test for user report", () => {
  const users = [
    // => Test data
    { id: 1, name: "Alice", email: "alice@example.com", role: "Admin" },
    { id: 2, name: "Bob", email: "bob@example.com", role: "User" },
  ];

  const report = generateReport(users); // => Generate report

  verify(report); // => Compare against approved baseline
}); // => On first run, saves baseline; future runs compare

Refactored: Approval testing with multiple scenarios

describe("Report approval tests", () => {
  test("approves empty report", () => {
    const report = generateReport([]); // => Empty user list
    verify(report); // => Baseline: header only
  });

  test("approves single user report", () => {
    const users = [{ id: 1, name: "Alice", email: "alice@example.com", role: "Admin" }];
    const report = generateReport(users);
    verify(report); // => Baseline: one user section
  });

  test("approves multi-user report", () => {
    const users = [
      { id: 1, name: "Alice", email: "alice@example.com", role: "Admin" },
      { id: 2, name: "Bob", email: "bob@example.com", role: "User" },
      { id: 3, name: "Charlie", email: "charlie@example.com", role: "Guest" },
    ];
    const report = generateReport(users);
    verify(report); // => Baseline: three user sections
  });
});

Key Takeaway: Approval testing captures complex outputs as baseline files. First run creates baseline, subsequent runs compare against it. Ideal for reports, generated code, or large data structures.

Why It Matters: Manual verification of complex outputs is error-prone. Approval testing automates this - Approval testing can catch formatting regressions that manual reviews miss.

Example 61: Working with Seams in Untestable Code

Seams are places where you can alter program behavior without modifying code. Identify seams to inject test doubles into legacy code.

Red: Untestable code with hard dependencies

class OrderProcessor {
  processOrder(orderId: number): boolean {
    const db = new Database(); // => FAIL: Hard-coded dependency
    const order = db.getOrder(orderId); // => Cannot test without real DB

    const payment = new PaymentGateway(); // => FAIL: Hard-coded payment
    return payment.charge(order.total); // => Cannot test without real gateway
  }
}

Green: Identify seams and inject dependencies

interface Database {
  getOrder(id: number): { id: number; total: number };
}

interface PaymentGateway {
  charge(amount: number): boolean;
}

class OrderProcessor {
  constructor(
    // => Seam 1: Constructor injection
    private db: Database,
    private payment: PaymentGateway,
  ) {}

  processOrder(orderId: number): boolean {
    const order = this.db.getOrder(orderId); // => Uses injected DB
    return this.payment.charge(order.total); // => Uses injected gateway
  }
}

test("processOrder with test doubles", () => {
  const fakeDb = {
    // => Fake database seam
    getOrder: (id: number) => ({ id, total: 100 }),
  };

  const fakePayment = {
    // => Fake payment seam
    charge: (amount: number) => amount < 1000,
  };

  const processor = new OrderProcessor(fakeDb, fakePayment); // => Inject seams

  const result = processor.processOrder(1);

  expect(result).toBe(true); // => Verify behavior
}); // => Tested without real dependencies

Key Takeaway: Seams enable testing by providing injection points for test doubles. Constructor injection, method parameters, and callbacks are common seams.

Why It Matters: Legacy code often has hard-coded dependencies making it untestable. Identifying seams enables gradual testability improvements without full rewrites - Enterprise platforms use seam identification to introduce testing into legacy codebases, enabling safe modernization.

Example 62: Dependency Breaking Techniques

Breaking dependencies makes legacy code testable. Use extract method, extract interface, and parameterize constructor to create seams.

Red: Class with multiple hard dependencies

class UserService {
  createUser(name: string, email: string): void {
    // => Multiple hard dependencies
    const db = new Database(); // => Hard dependency 1
    const emailer = new EmailService(); // => Hard dependency 2
    const logger = new Logger(); // => Hard dependency 3

    db.saveUser({ name, email }); // => Cannot test
    emailer.sendWelcome(email); // => Cannot test
    logger.log(`User created: ${name}`); // => Cannot test
  }
}

Green: Extract and inject dependencies

interface Database {
  saveUser(user: { name: string; email: string }): void;
}

interface EmailService {
  sendWelcome(email: string): void;
}

interface Logger {
  log(message: string): void;
}

class UserService {
  constructor(
    // => Parameterize constructor
    private db: Database,
    private emailer: EmailService,
    private logger: Logger,
  ) {}

  createUser(name: string, email: string): void {
    this.db.saveUser({ name, email }); // => Uses injected DB
    this.emailer.sendWelcome(email); // => Uses injected emailer
    this.logger.log(`User created: ${name}`); // => Uses injected logger
  }
}

test("createUser calls all services", () => {
  const mockDb = {
    saveUser: jest.fn(),
  }; // => Mock database
  const mockEmailer = {
    sendWelcome: jest.fn(),
  }; // => Mock emailer
  const mockLogger = {
    log: jest.fn(),
  }; // => Mock logger

  const service = new UserService(mockDb, mockEmailer, mockLogger); // => Inject mocks
  service.createUser("Alice", "alice@example.com");

  expect(mockDb.saveUser).toHaveBeenCalledWith({ name: "Alice", email: "alice@example.com" });
  expect(mockEmailer.sendWelcome).toHaveBeenCalledWith("alice@example.com");
  expect(mockLogger.log).toHaveBeenCalledWith("User created: Alice");
}); // => All interactions verified

Key Takeaway: Break hard dependencies by extracting interfaces and injecting through constructor. This creates seams for testing without changing core logic.

Why It Matters: Hard dependencies block testing and refactoring. Dependency breaking enables incremental improvement - Twitter’s backend team documented significantly faster test execution after breaking database dependencies using these techniques.

Example 63: TDD for Microservices - Service Isolation

Microservices require testing individual services in isolation without full environment. Use contract testing and service virtualization.

Red: Test service depending on other microservices

test("UserService gets user orders", async () => {
  const userService = new UserService(); // => FAILS: Needs OrderService running
  const orders = await userService.getUserOrders(1);
  expect(orders).toHaveLength(2);
});

Green: Mock HTTP client for service isolation

interface HttpClient {
  get(url: string): Promise<any>;
}

class UserService {
  constructor(
    private http: HttpClient,
    private orderServiceUrl: string,
  ) {}

  async getUserOrders(userId: number): Promise<any[]> {
    const response = await this.http.get(`${this.orderServiceUrl}/users/${userId}/orders`);
    // => Calls order service
    return response.data; // => Returns order data
  }
}

test("UserService gets user orders via HTTP", async () => {
  const mockHttp = {
    // => Mock HTTP client
    get: jest.fn().mockResolvedValue({
      // => Mock response
      data: [
        { id: 1, total: 100 },
        { id: 2, total: 200 },
      ],
    }),
  };

  const userService = new UserService(mockHttp, "http://order-service"); // => Inject mock
  const orders = await userService.getUserOrders(1);

  expect(orders).toHaveLength(2); // => Verify result
  expect(mockHttp.get).toHaveBeenCalledWith("http://order-service/users/1/orders");
}); // => Tested without real OrderService

Key Takeaway: Test microservices in isolation by mocking HTTP clients. Inject service URLs and HTTP clients as dependencies to enable independent testing.

Why It Matters: Microservices with real service dependencies are slow and flaky. Service isolation enables fast unit testing - large-scale microservice architectures run many thousands of tests efficiently using service virtualization instead of full environments.

Example 64: Contract Testing for Microservices

Contract testing verifies service integrations match expected contracts without running both services. Producer defines contract, consumer tests against it.

Red: Integration test requiring both services

test("UserService integrates with OrderService", async () => {
  // => FAILS: Requires both services running
  const orderService = new OrderService(); // Real service
  const userService = new UserService(orderService);
  const orders = await userService.getUserOrders(1);
  expect(orders[0].id).toBe(1);
});

Green: Consumer-driven contract testing

// Contract definition
interface OrderServiceContract {
  "GET /users/:userId/orders": {
    response: {
      data: Array<{ id: number; total: number; status: string }>;
    };
  };
}

// Consumer test (UserService side)
test("UserService expects OrderService contract", async () => {
  const mockHttp = {
    get: jest.fn().mockResolvedValue({
      // => Contract shape
      data: [
        { id: 1, total: 100, status: "pending" }, // => Matches contract
        { id: 2, total: 200, status: "shipped" },
      ],
    }),
  };

  const userService = new UserService(mockHttp, "http://order-service");
  const orders = await userService.getUserOrders(1);

  expect(orders).toEqual([
    // => Verify contract structure
    { id: 1, total: 100, status: "pending" },
    { id: 2, total: 200, status: "shipped" },
  ]);
}); // => Consumer validates expected contract

// Provider test (OrderService side)
test("OrderService fulfills contract", async () => {
  const orderService = new OrderService();
  const response = await orderService.getOrdersByUserId(1);

  // Verify response matches contract
  expect(response.data).toEqual(
    expect.arrayContaining([
      expect.objectContaining({
        // => Contract fields required
        id: expect.any(Number),
        total: expect.any(Number),
        status: expect.any(String),
      }),
    ]),
  );
}); // => Provider guarantees contract compliance

Key Takeaway: Contract testing validates service integration expectations without running both services. Consumer defines expected contract, provider guarantees compliance.

Why It Matters: Integration testing all microservice combinations is exponentially expensive. Contract testing catches integration bugs without full environments - Pact (contract testing tool) users report significantly fewer integration failures in production.

Example 65: Testing Distributed Systems - Eventual Consistency

Distributed systems often have eventual consistency. Tests must account for asynchronous propagation and race conditions.

Red: Test assumes immediate consistency

test("replication happens immediately", async () => {
  await primaryDb.write({ id: 1, name: "Alice" }); // => Write to primary
  const data = await replicaDb.read(1); // => FAILS: Replica not updated yet
  expect(data.name).toBe("Alice"); // Race condition
});

Green: Test eventual consistency with polling

async function waitForReplication<T>(
  fetchFn: () => Promise<T>,
  predicate: (value: T) => boolean,
  timeout = 5000,
): Promise<T> {
  // => Poll until condition met
  const startTime = Date.now();

  while (Date.now() - startTime < timeout) {
    // => Keep trying until timeout
    const value = await fetchFn(); // => Fetch current value
    if (predicate(value)) {
      return value;
    } // => Return when predicate true
    await new Promise((resolve) => setTimeout(resolve, 100)); // => Wait 100ms
  }

  throw new Error("Timeout waiting for replication"); // => Failed to meet condition
}

test("replication happens eventually", async () => {
  await primaryDb.write({ id: 1, name: "Alice" }); // => Write to primary

  const data = await waitForReplication(
    () => replicaDb.read(1), // => Fetch function
    (value) => value !== null && value.name === "Alice", // => Predicate
    5000, // => 5 second timeout
  ); // => Polls until replicated

  expect(data.name).toBe("Alice"); // => Verify after eventual consistency
}); // => Test accounts for async propagation

Key Takeaway: Test eventual consistency with polling and timeouts. Don’t assume immediate consistency in distributed systems - wait for conditions to be met.

Why It Matters: Distributed systems have inherent delays. Tests assuming immediate consistency are flaky - Eventual consistency helpers can eliminate most timing-related test failures.

Example 66: Testing Event Sourcing Systems

Event sourcing stores state changes as events. TDD for event sourcing tests event application and state reconstruction.

Red: Test event sourcing without infrastructure

test("applies events to rebuild state", () => {
  const account = new Account(); // => FAILS: Needs event store
  account.applyEvent({ type: "Deposited", amount: 100 });
  expect(account.getBalance()).toBe(100);
});

Green: Event sourcing with in-memory event store

type Event = { type: "Deposited"; amount: number } | { type: "Withdrawn"; amount: number };

class Account {
  private balance = 0;

  applyEvent(event: Event): void {
    // => Apply event to state
    if (event.type === "Deposited") {
      this.balance += event.amount; // => Increase balance
    } else if (event.type === "Withdrawn") {
      this.balance -= event.amount; // => Decrease balance
    }
  }

  getBalance(): number {
    return this.balance;
  }
}

test("applies deposit events", () => {
  const account = new Account();
  account.applyEvent({ type: "Deposited", amount: 100 }); // => Apply deposit
  expect(account.getBalance()).toBe(100); // => Balance increased
});

test("applies multiple events in order", () => {
  const account = new Account();
  account.applyEvent({ type: "Deposited", amount: 100 }); // => Deposit 100
  account.applyEvent({ type: "Withdrawn", amount: 30 }); // => Withdraw 30
  account.applyEvent({ type: "Deposited", amount: 50 }); // => Deposit 50

  expect(account.getBalance()).toBe(120); // => 100 - 30 + 50 = 120
}); // => Events applied in sequence

Refactored: Event store with event replay

class EventStore {
  private events: Map<string, Event[]> = new Map(); // => In-memory event storage

  append(streamId: string, event: Event): void {
    if (!this.events.has(streamId)) {
      this.events.set(streamId, []); // => Initialize stream
    }
    this.events.get(streamId)!.push(event); // => Append event
  }

  getEvents(streamId: string): Event[] {
    return this.events.get(streamId) || []; // => Retrieve event stream
  }
}

function replayEvents(events: Event[]): Account {
  // => Rebuild state from events
  const account = new Account();
  events.forEach((event) => account.applyEvent(event)); // => Apply each event
  return account; // => Return reconstructed state
}

test("rebuilds state from event store", () => {
  const store = new EventStore();
  const accountId = "account-1";

  store.append(accountId, { type: "Deposited", amount: 100 });
  store.append(accountId, { type: "Withdrawn", amount: 30 });
  store.append(accountId, { type: "Deposited", amount: 50 });

  const events = store.getEvents(accountId); // => Retrieve all events
  const account = replayEvents(events); // => Rebuild from events

  expect(account.getBalance()).toBe(120); // => State reconstructed correctly
});

Key Takeaway: Test event sourcing by applying events and verifying state. Use in-memory event stores for fast testing without infrastructure.

Why It Matters: Event sourcing enables audit trails and temporal queries. TDD ensures correct event application - Event sourcing tests can catch many bugs during development that would be catastrophic in production.

Example 67: Testing CQRS Patterns

CQRS (Command Query Responsibility Segregation) separates read and write models. TDD tests commands and queries independently.

Red: Test CQRS without separation

test("updates and queries user", () => {
  const service = new UserService(); // => FAILS: Needs read/write separation
  service.updateUser(1, { name: "Alice" });
  const user = service.getUser(1);
  expect(user.name).toBe("Alice");
});

Green: Separate command and query models

// Write model (Commands)
interface UpdateUserCommand {
  userId: number;
  data: { name: string };
}

class UserCommandHandler {
  private writeDb: Map<number, any> = new Map(); // => Write model storage

  handle(command: UpdateUserCommand): void {
    this.writeDb.set(command.userId, command.data); // => Store update
    // In production: publish event to sync read model
  }
}

// Read model (Queries)
interface UserQuery {
  userId: number;
}

class UserQueryHandler {
  private readDb: Map<number, any> = new Map(); // => Read model storage

  query(query: UserQuery): any {
    return this.readDb.get(query.userId); // => Retrieve from read model
  }

  syncFromEvent(userId: number, data: any): void {
    // => Sync from event
    this.readDb.set(userId, data); // => Update read model
  }
}

describe("CQRS pattern", () => {
  test("command handler processes updates", () => {
    const commandHandler = new UserCommandHandler();
    commandHandler.handle({ userId: 1, data: { name: "Alice" } }); // => Execute command
    // Command side doesn't return data, only processes
  });

  test("query handler retrieves data", () => {
    const queryHandler = new UserQueryHandler();
    queryHandler.syncFromEvent(1, { name: "Alice" }); // => Simulate event sync

    const user = queryHandler.query({ userId: 1 }); // => Execute query
    expect(user.name).toBe("Alice"); // => Verify read model
  });
});

Key Takeaway: Test commands and queries separately in CQRS. Commands modify state, queries read optimized views. Test synchronization between write and read models.

Why It Matters: CQRS enables independent scaling of reads and writes. Separate testing prevents coupling - Enterprise services using CQRS can reduce cross-concern bugs with separate test suites.

Example 68: TDD in Polyglot Environments

Teams using multiple languages need consistent TDD practices across languages. Adapt patterns to language idioms while maintaining discipline.

Challenge: Consistent TDD across Java and TypeScript

Java approach (JUnit 5):

@Test
void calculateDiscount_appliesCorrectRate() {
    // => Given
    PriceCalculator calculator = new PriceCalculator();
    // => Calculator instance created

    // => When
    BigDecimal result = calculator.calculateDiscount(
        new BigDecimal("100"),
        new BigDecimal("0.1")
    );
    // => Calls calculateDiscount with 100 and 10% rate

    // => Then
    assertEquals(
        new BigDecimal("90.00"),
        result.setScale(2, RoundingMode.HALF_UP)
    );
    // => Verifies result is 90.00 (100 - 10%)
}

TypeScript approach (Jest):

test("calculateDiscount applies correct rate", () => {
  // => Arrange
  const calculator = new PriceCalculator();
  // => Calculator instance created

  // => Act
  const result = calculator.calculateDiscount(100, 0.1);
  // => Calls calculateDiscount with 100 and 10% rate

  // => Assert
  expect(result).toBe(90); // => Verifies result is 90 (100 - 10%)
});

Key Similarities (Cross-Language TDD):

1. **Arrange-Act-Assert pattern** - Both use AAA structure
2. **Descriptive test names** - `calculateDiscount_appliesCorrectRate` vs `calculateDiscount applies correct rate`
3. **Single assertion focus** - Each test verifies one behavior
4. **Fast execution** - Both run in milliseconds
5. **Red-Green-Refactor** - Same TDD cycle

Language-Specific Adaptations:

| Aspect          | Java                      | TypeScript                  |
| --------------- | ------------------------- | --------------------------- |
| **Test Runner** | JUnit 5                   | Jest                        |
| **Mocking**     | Mockito                   | jest.fn()                   |
| **Assertions**  | assertEquals, assertTrue  | expect().toBe(), .toEqual() |
| **Async**       | CompletableFuture         | async/await                 |
| **Type Safety** | Compile-time (static)     | Compile-time (TypeScript)   |
| **Naming**      | camelCase_withUnderscores | camelCase with spaces       |
| **Lifecycle**   | @BeforeEach, @AfterEach   | beforeEach(), afterEach()   |

Key Takeaway: TDD principles (Red-Green-Refactor, AAA pattern, single assertion) are universal. Adapt to language idioms while maintaining core discipline.

Why It Matters: Polyglot teams risk inconsistent testing across languages. Universal TDD principles maintain quality - Enterprise polyglot codebases use language-specific tools but consistent TDD patterns to achieve high coverage across all languages.

Example 69: Performance-Sensitive TDD

Performance-critical code needs TDD without sacrificing optimization. Write performance tests alongside functional tests.

Red: Functional test without performance constraint

test("sorts array correctly", () => {
  const input = [3, 1, 4, 1, 5, 9, 2, 6];
  const result = sort(input); // => FAILS: Function not defined
  expect(result).toEqual([1, 1, 2, 3, 4, 5, 6, 9]);
});

Green: Naive implementation (functional but slow)

function sort(arr: number[]): number[] {
  // => Bubble sort (O(n²))
  const result = [...arr]; // => Copy array
  for (let i = 0; i < result.length; i++) {
    // => Outer loop
    for (let j = 0; j < result.length - 1; j++) {
      // => Inner loop
      if (result[j] > result[j + 1]) {
        // => Compare adjacent
        [result[j], result[j + 1]] = [result[j + 1], result[j]]; // => Swap
      }
    }
  }
  return result; // => Sorted array
}

test("sorts array correctly", () => {
  const input = [3, 1, 4, 1, 5, 9, 2, 6];
  const result = sort(input);
  expect(result).toEqual([1, 1, 2, 3, 4, 5, 6, 9]); // => Passes
});

Refactor: Add performance constraint and optimize

function sort(arr: number[]): number[] {
  // => Native sort (O(n log n))
  return [...arr].sort((a, b) => a - b); // => Optimized algorithm
}

describe("sort", () => {
  test("sorts array correctly", () => {
    const input = [3, 1, 4, 1, 5, 9, 2, 6];
    const result = sort(input);
    expect(result).toEqual([1, 1, 2, 3, 4, 5, 6, 9]); // => Functional correctness
  });

  test("sorts large array within performance constraint", () => {
    const input = Array.from({ length: 10000 }, () => Math.random()); // => 10k items
    const startTime = Date.now();

    const result = sort(input); // => Sort large array

    const duration = Date.now() - startTime; // => Measure time
    expect(duration).toBeLessThan(100); // => Must complete in <100ms
    expect(result[0]).toBeLessThanOrEqual(result[result.length - 1]); // => Verify sorted
  }); // => Performance test
});

Key Takeaway: Start with functional tests, then add performance constraints. Optimize implementation while keeping functional tests passing.

Why It Matters: Premature optimization leads to complex code without proven need. TDD enables optimization when needed with safety net - V8 JavaScript engine development uses performance tests to prevent regressions during optimizations.

Example 70: Security Testing with TDD

Security requirements need TDD just like functional requirements. Test authentication, authorization, input validation, and encryption.

Red: Test authentication requirement

test("rejects unauthenticated access", () => {
  const middleware = authMiddleware(); // => FAILS: Not defined
  const req = { headers: {} }; // No auth header
  const res = { status: jest.fn(), send: jest.fn() };

  middleware(req, res, jest.fn());

  expect(res.status).toHaveBeenCalledWith(401);
});

Green: Authentication middleware

interface Request {
  headers: { authorization?: string };
}

interface Response {
  status(code: number): Response;
  send(body: any): void;
}

function authMiddleware() {
  return (req: Request, res: Response, next: () => void) => {
    if (!req.headers.authorization) {
      // => Check authorization header
      res.status(401).send({ error: "Unauthorized" }); // => Reject
      return;
    }

    const token = req.headers.authorization.replace("Bearer ", ""); // => Extract token
    if (token !== "valid-token") {
      // => Validate token
      res.status(403).send({ error: "Forbidden" }); // => Invalid token
      return;
    }

    next(); // => Allow request
  };
}

describe("authMiddleware", () => {
  test("rejects unauthenticated access", () => {
    const middleware = authMiddleware();
    const req = { headers: {} };
    const res = { status: jest.fn().mockReturnThis(), send: jest.fn() };

    middleware(req as Request, res as Response, jest.fn());

    expect(res.status).toHaveBeenCalledWith(401); // => Unauthorized
    expect(res.send).toHaveBeenCalledWith({ error: "Unauthorized" });
  });

  test("rejects invalid token", () => {
    const middleware = authMiddleware();
    const req = { headers: { authorization: "Bearer invalid" } };
    const res = { status: jest.fn().mockReturnThis(), send: jest.fn() };

    middleware(req as Request, res as Response, jest.fn());

    expect(res.status).toHaveBeenCalledWith(403); // => Forbidden
  });

  test("allows valid token", () => {
    const middleware = authMiddleware();
    const req = { headers: { authorization: "Bearer valid-token" } };
    const res = { status: jest.fn(), send: jest.fn() };
    const next = jest.fn();

    middleware(req as Request, res as Response, next);

    expect(next).toHaveBeenCalled(); // => Request allowed
  });
});

Key Takeaway: Test security requirements (authentication, authorization, validation) with same TDD rigor as functional features. Write failing security test, implement protection, verify it works.

Why It Matters: Security bugs are often logic errors testable through TDD. Research indicates that

Example 71: TDD Anti-Patterns - Testing Implementation Details

Testing implementation details makes tests brittle. Focus on behavior and public API, not internal structure.

ANTI-PATTERN: Testing private implementation

class ShoppingCart {
  private items: any[] = []; // => Private field

  addItem(item: any): void {
    this.items.push(item); // => Internal implementation
  }

  getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

// FAIL: BAD - Tests private implementation
test("adds item to items array", () => {
  const cart = new ShoppingCart();
  cart.addItem({ id: 1, price: 10 });

  expect(cart["items"]).toHaveLength(1); // => Accesses private field
  expect(cart["items"][0].price).toBe(10); // => Tests internal structure
}); // => Brittle test, breaks when implementation changes

CORRECT PATTERN: Test public behavior

// PASS: GOOD - Tests observable behavior
describe("ShoppingCart", () => {
  test("adds item and increases total", () => {
    const cart = new ShoppingCart();
    cart.addItem({ id: 1, price: 10 }); // => Public API

    expect(cart.getTotal()).toBe(10); // => Observable behavior
  }); // => Tests what cart does, not how it does it

  test("calculates total for multiple items", () => {
    const cart = new ShoppingCart();
    cart.addItem({ id: 1, price: 10 });
    cart.addItem({ id: 2, price: 20 });

    expect(cart.getTotal()).toBe(30); // => Behavior test
  });
});

Comparison:

| Approach            | Implementation Testing (BAD)       | Behavior Testing (GOOD) |
| ------------------- | ---------------------------------- | ----------------------- |
| **What it tests**   | Internal structure (items array)   | Public behavior (total) |
| **Coupling**        | Tightly coupled to internals       | Coupled to API only     |
| **Refactor safety** | Breaks when implementation changes | Survives refactoring    |
| **Value**           | Tests how it works                 | Tests what it does      |
| **Maintenance**     | High (brittle tests)               | Low (stable tests)      |

Key Takeaway: Test observable behavior through public API, not internal implementation. Tests should verify what the code does for users, not how it achieves it internally.

Why It Matters: Tests coupled to implementation details break during refactoring, discouraging improvement. Behavior-focused tests enable safe refactoring - Kent Beck’s TDD philosophy emphasizes testing observable behavior to maintain flexibility.

Example 72: Test-Induced Design Damage

Over-application of TDD can lead to unnecessary complexity. Balance testability with design simplicity.

ANTI-PATTERN: Over-engineering for testability

// FAIL: BAD - Excessive abstraction for testability
interface TimeProvider {
  now(): Date;
}

interface RandomProvider {
  random(): number;
}

interface LoggerProvider {
  log(message: string): void;
}

class UserService {
  constructor(
    private timeProvider: TimeProvider,
    private randomProvider: RandomProvider,
    private loggerProvider: LoggerProvider,
    // ... 5 more providers
  ) {} // => 8 constructor parameters for simple service

  createUser(name: string): any {
    const id = Math.floor(this.randomProvider.random() * 1000000);
    const createdAt = this.timeProvider.now();
    this.loggerProvider.log(`User created: ${name}`);
    return { id, name, createdAt };
  }
}

CORRECT PATTERN: Pragmatic testability

// PASS: GOOD - Inject only what needs variation in tests
class UserService {
  constructor(private idGenerator: () => number = () => Math.floor(Math.random() * 1000000)) {
    // => Only inject what varies in tests
  }

  createUser(name: string): any {
    const id = this.idGenerator(); // => Injected dependency
    const createdAt = new Date(); // => Simple, deterministic in most tests
    console.log(`User created: ${name}`); // => Logging doesn't affect behavior
    return { id, name, createdAt };
  }
}

test("createUser generates unique ID", () => {
  let nextId = 1;
  const service = new UserService(() => nextId++); // => Inject only ID generator

  const user1 = service.createUser("Alice");
  const user2 = service.createUser("Bob");

  expect(user1.id).toBe(1); // => Deterministic ID
  expect(user2.id).toBe(2); // => Controlled behavior
}); // => Simple test without excessive mocking

Comparison:

| Aspect              | Over-Engineered          | Pragmatic                  |
| ------------------- | ------------------------ | -------------------------- |
| **Constructor**     | 8+ parameters            | 1 parameter                |
| **Complexity**      | High (many abstractions) | Low (focused abstractions) |
| **Testability**     | 100% mockable            | Mockable where needed      |
| **Production Code** | Complex setup            | Simple setup               |
| **Maintenance**     | High (many interfaces)   | Low (minimal abstractions) |

Key Takeaway: Inject dependencies that vary in tests (IDs, external services). Don’t inject stable utilities (logging, date formatting). Balance testability with simplicity.

Why It Matters: Over-abstraction for testability creates complex production code. Pragmatic TDD focuses on testing business logic - DHH’s “TDD is dead” debate highlighted test-induced damage when testability trumps design clarity.

Example 73: Scaling TDD Across Teams

Large organizations need consistent TDD practices across teams. Establish shared standards, CI enforcement, and knowledge sharing.

Challenge: 50+ teams with inconsistent testing practices

Solution Framework:

// 1. Shared Testing Standards (documented in wiki/guide)
const TESTING_STANDARDS = {
  coverage: {
    minimum: 80, // => 80% line coverage required
    target: 95, // => 95% target for critical paths
  },
  naming: {
    pattern: "descriptive test names with spaces", // => Readable names
    structure: "should [expected behavior] when [condition]",
  },
  organization: {
    pattern: "describe → test hierarchy", // => Consistent structure
    oneAssertPerTest: false, // => Allow related assertions
  },
};

// 2. CI Enforcement (in pipeline config)
test("CI enforces coverage threshold", () => {
  const coverageReport = {
    // => Mock coverage report
    lines: { pct: 85 },
    statements: { pct: 84 },
    functions: { pct: 86 },
    branches: { pct: 82 },
  };

  const meetsThreshold = Object.values(coverageReport).every((metric) => metric.pct >= 80);
  // => Check all metrics

  expect(meetsThreshold).toBe(true); // => Enforced in CI
}); // => Build fails if coverage drops

// 3. Shared Test Utilities (npm package)
class TestHelpers {
  static createMockUser(overrides = {}): any {
    // => Shared test data builder
    return {
      id: 1,
      name: "Test User",
      email: "test@example.com",
      role: "user",
      ...overrides, // => Customizable fields
    };
  }

  static waitFor(condition: () => boolean, timeout = 5000): Promise<void> {
    // => Shared async helper
    return new Promise((resolve, reject) => {
      const startTime = Date.now();
      const check = () => {
        if (condition()) {
          resolve();
        } else if (Date.now() - startTime > timeout) {
          reject(new Error("Timeout"));
        } else {
          setTimeout(check, 100);
        }
      };
      check();
    });
  }
}

// 4. Cross-Team Knowledge Sharing
describe("Testing Guild Practices", () => {
  test("teams share testing patterns", () => {
    const guild = {
      // => Testing guild structure
      members: ["Team A", "Team B", "Team C"],
      meetings: "bi-weekly",
      activities: ["pattern sharing", "code reviews", "training"],
    };

    expect(guild.activities).toContain("pattern sharing"); // => Knowledge transfer
  });
});

Implementation Checklist:

- [ ] Document testing standards (wiki, guide)
- [ ] Enforce coverage thresholds in CI (80% minimum)
- [ ] Create shared test utility packages
- [ ] Establish testing guild for cross-team learning
- [ ] Implement pre-commit hooks for test quality
- [ ] Run periodic test health reviews
- [ ] Provide TDD training for new team members
- [ ] Share success stories and metrics

Key Takeaway: Scale TDD through documented standards, CI enforcement, shared utilities, and cross-team knowledge sharing. Consistency emerges from infrastructure and culture.

Why It Matters: Inconsistent testing across teams creates quality gaps. Standardization scales quality - Consistent testing practices across large engineering organizations can maintain high coverage.

Example 74: TDD Coaching and Mentoring

Teaching TDD requires hands-on practice, not lectures. Use pair programming, code reviews, and kata exercises.

Anti-Pattern: Lecture-Based Teaching

FAIL: BAD Approach:

1. Presentation: "Introduction to TDD" (60 minutes)
2. Slides: Red-Green-Refactor cycle (30 slides)
3. Demo: Instructor codes alone (20 minutes)
4. Q&A: Students ask questions (10 minutes)
5. Result: Students understand theory, can't apply it

Effective Pattern: Practice-Based Learning

// 1. Kata Exercise: FizzBuzz (Pair Programming)
describe("FizzBuzz Kata", () => {
  // Kata steps:
  // - Driver: Write failing test
  // - Navigator: Suggest minimal implementation
  // - Switch roles every 5 minutes
  // - Complete 15 rules in 75 minutes

  test("returns number for non-multiples", () => {
    expect(fizzBuzz(1)).toBe("1"); // => Failing test (Red)
  });

  // Student implements minimal code (Green)
  function fizzBuzz(n: number): string {
    return String(n); // => Simplest implementation
  }

  test("returns Fizz for multiples of 3", () => {
    expect(fizzBuzz(3)).toBe("Fizz"); // => Next failing test
  });

  // Student refactors implementation (Refactor)
});

// 2. Code Review Feedback (Real Production Code)
class CodeReviewFeedback {
  // FAIL: BAD - Testing implementation details
  test_BAD_example() {
    const cart = new ShoppingCart();
    cart.addItem({ price: 10 });
    expect(cart["items"].length).toBe(1); // => Reviewer flags this
  }

  // PASS: GOOD - Testing behavior
  test_GOOD_example() {
    const cart = new ShoppingCart();
    cart.addItem({ price: 10 });
    expect(cart.getTotal()).toBe(10); // => Reviewer approves
  }
}

// 3. Mobbing Session (Team Learning)
describe("Mob Programming TDD", () => {
  // Setup:
  // - One driver (keyboard)
  // - 4-6 navigators (giving directions)
  // - Rotate driver every 10 minutes
  // - Solve real production problem as team

  test("team solves problem together", () => {
    const mobSession = {
      driver: "Alice",
      navigators: ["Bob", "Charlie", "Diana"],
      problem: "Implement user authentication",
      learnings: ["TDD rhythm", "Test design", "Refactoring"],
    };

    expect(mobSession.learnings).toContain("TDD rhythm"); // => Collective learning
  });
});

Teaching Framework:

| Week | Activity                          | Duration | Focus                     |
| ---- | --------------------------------- | -------- | ------------------------- |
| 1    | FizzBuzz Kata (Pair Programming)  | 90 min   | Red-Green-Refactor rhythm |
| 2    | String Calculator Kata            | 90 min   | Test design               |
| 3    | Bowling Game Kata                 | 90 min   | Refactoring               |
| 4    | Production Code (Mob Programming) | 2 hours  | Real-world application    |
| 5    | Code Reviews with TDD Feedback    | Ongoing  | Continuous improvement    |

Key Takeaway: Teach TDD through hands-on practice (katas, pairing, mobbing) rather than lectures. Learning happens through doing, not listening.

Why It Matters: TDD is a skill requiring muscle memory. Theory without practice doesn’t stick - Uncle Bob Martin’s TDD training uses katas exclusively, with 90%+ participants applying TDD after kata practice versus 30% after lecture-only training.

Example 75: ROI Measurement for TDD

Measuring TDD return on investment requires tracking defect rates, development velocity, and maintenance costs.

Metrics to Track:

interface TDDMetrics {
  defectDensity: number; // => Defects per 1000 lines of code
  developmentVelocity: number; // => Story points per sprint
  maintenanceCost: number; // => Hours spent on bug fixes
  testCoverage: number; // => Percentage of code covered
  cycleTime: number; // => Time from commit to production
}

// Before TDD baseline (Month 0)
const beforeTDD: TDDMetrics = {
  defectDensity: 15, // => 15 defects per 1000 LOC
  developmentVelocity: 20, // => 20 story points per sprint
  maintenanceCost: 120, // => 120 hours per month on bugs
  testCoverage: 40, // => 40% coverage
  cycleTime: 168, // => 1 week (hours)
};

// After TDD adoption (Month 6)
const afterTDD: TDDMetrics = {
  defectDensity: 3, // => 3 defects per 1000 LOC (80% reduction)
  developmentVelocity: 28, // => 28 story points per sprint (40% increase)
  maintenanceCost: 30, // => 30 hours per month on bugs (75% reduction)
  testCoverage: 92, // => 92% coverage
  cycleTime: 24, // => 1 day (hours) (86% reduction)
};

test("calculates TDD ROI", () => {
  const defectReduction = ((beforeTDD.defectDensity - afterTDD.defectDensity) / beforeTDD.defectDensity) * 100;
  // => 80% defect reduction

  const velocityIncrease =
    ((afterTDD.developmentVelocity - beforeTDD.developmentVelocity) / beforeTDD.developmentVelocity) * 100;
  // => 40% velocity increase

  const maintenanceSavings = ((beforeTDD.maintenanceCost - afterTDD.maintenanceCost) / beforeTDD.maintenanceCost) * 100;
  // => 75% maintenance cost reduction

  expect(defectReduction).toBeGreaterThan(70); // => Significant quality improvement
  expect(velocityIncrease).toBeGreaterThan(30); // => Productivity gain
  expect(maintenanceSavings).toBeGreaterThan(60); // => Cost savings
});

// ROI Calculation
function calculateTDDROI(before: TDDMetrics, after: TDDMetrics): number {
  const costReduction = before.maintenanceCost - after.maintenanceCost; // => 90 hours saved
  const hourlyRate = 75; // => Average developer hourly rate
  const monthlySavings = costReduction * hourlyRate; // => substantial amounts per month

  const tddInvestment = 40; // => 40 hours TDD training cost
  const investmentCost = tddInvestment * hourlyRate; // => substantial amounts one-time

  const paybackMonths = investmentCost / monthlySavings; // => 0.44 months (2 weeks)
  return paybackMonths; // => ROI achieved in < 1 month
}

test("TDD investment pays back quickly", () => {
  const payback = calculateTDDROI(beforeTDD, afterTDD);
  expect(payback).toBeLessThan(1); // => Pays back in under 1 month
});

Key Takeaway: Measure TDD ROI through defect reduction, velocity increase, and maintenance cost savings. Track metrics before and after adoption to quantify value.

Why It Matters: Teams need business justification for TDD investment. Data-driven ROI measurement enables informed decisions - Research indicates that

Example 76: TDD in Regulated Industries

Regulated industries (finance, healthcare, aerospace) require audit trails and compliance testing. TDD provides documentation and traceability.

Red: Test regulatory requirement

test("logs all financial transactions for audit", () => {
  const processor = new TransactionProcessor(); // => FAILS: No audit logging
  processor.processPayment(100, "USD");
  const auditLog = processor.getAuditLog();
  expect(auditLog).toHaveLength(1);
});

Green: Implement audit logging

interface AuditEntry {
  timestamp: Date;
  action: string;
  amount: number;
  currency: string;
  userId: string;
}

class TransactionProcessor {
  private auditLog: AuditEntry[] = []; // => Audit trail storage

  processPayment(amount: number, currency: string, userId = "system"): void {
    // Process payment logic
    this.logAudit("PAYMENT_PROCESSED", amount, currency, userId); // => Audit logging
  }

  private logAudit(action: string, amount: number, currency: string, userId: string): void {
    this.auditLog.push({
      // => Immutable audit entry
      timestamp: new Date(),
      action,
      amount,
      currency,
      userId,
    });
  }

  getAuditLog(): AuditEntry[] {
    return [...this.auditLog]; // => Return copy (immutable)
  }
}

describe("TransactionProcessor audit compliance", () => {
  test("logs all financial transactions", () => {
    const processor = new TransactionProcessor();
    processor.processPayment(100, "USD", "user-123");

    const auditLog = processor.getAuditLog();
    expect(auditLog).toHaveLength(1); // => One entry logged
    expect(auditLog[0].action).toBe("PAYMENT_PROCESSED");
    expect(auditLog[0].amount).toBe(100);
    expect(auditLog[0].currency).toBe("USD");
    expect(auditLog[0].userId).toBe("user-123");
  }); // => Regulatory requirement tested

  test("audit log is immutable", () => {
    const processor = new TransactionProcessor();
    processor.processPayment(100, "USD");

    const log1 = processor.getAuditLog();
    const log2 = processor.getAuditLog();

    expect(log1).not.toBe(log2); // => Different array instances
    expect(log1).toEqual(log2); // => Same content
  }); // => Prevents tampering
});

Key Takeaway: TDD for regulatory compliance tests audit trails, data integrity, and traceability requirements. Tests serve as executable documentation for auditors.

Why It Matters: Regulatory violations have severe consequences (fines, shutdowns). TDD provides audit-ready documentation - NASA’s JPL Software Development Process requires 100% test coverage for flight software, with tests serving as compliance evidence.

Example 77: Compliance Testing Patterns

Compliance testing verifies regulatory requirements (GDPR, HIPAA, SOC2). Test data retention, access controls, and encryption.

Red: Test GDPR data deletion requirement

test("deletes user data on request (GDPR Right to be Forgotten)", () => {
  const userService = new UserService(); // => FAILS: No deletion logic
  userService.createUser("alice@example.com", { name: "Alice" });
  userService.deleteUserData("alice@example.com");

  const user = userService.getUser("alice@example.com");
  expect(user).toBeNull(); // GDPR: User data must be deleted
});

Green: Implement compliant deletion

interface UserData {
  email: string;
  name: string;
  createdAt: Date;
}

class UserService {
  private users: Map<string, UserData> = new Map();
  private deletionLog: Array<{ email: string; deletedAt: Date }> = [];

  createUser(email: string, data: Omit<UserData, "email" | "createdAt">): void {
    this.users.set(email, {
      // => Store user data
      email,
      ...data,
      createdAt: new Date(),
    });
  }

  deleteUserData(email: string): void {
    this.users.delete(email); // => Delete user data
    this.deletionLog.push({
      // => Log deletion for compliance
      email,
      deletedAt: new Date(),
    });
  }

  getUser(email: string): UserData | null {
    return this.users.get(email) || null;
  }

  getDeletionLog(): Array<{ email: string; deletedAt: Date }> {
    return [...this.deletionLog]; // => Audit trail
  }
}

describe("GDPR Compliance", () => {
  test("deletes user data on request", () => {
    const service = new UserService();
    service.createUser("alice@example.com", { name: "Alice" });
    service.deleteUserData("alice@example.com");

    const user = service.getUser("alice@example.com");
    expect(user).toBeNull(); // => Data deleted
  });

  test("logs data deletion for audit", () => {
    const service = new UserService();
    service.createUser("alice@example.com", { name: "Alice" });
    service.deleteUserData("alice@example.com");

    const log = service.getDeletionLog();
    expect(log).toHaveLength(1); // => Deletion logged
    expect(log[0].email).toBe("alice@example.com");
  }); // => Compliance audit trail
});

Key Takeaway: Test compliance requirements (data deletion, access controls, encryption) with same rigor as features. Compliance tests document regulatory adherence.

Why It Matters: Non-compliance penalties are severe (GDPR: substantial penalties). Tested compliance prevents violations - Compliance testing can catch data handling violations before production.

Example 78: TDD for Machine Learning Systems

Machine learning systems need testing despite non-determinism. Test data pipelines, model interfaces, and prediction boundaries.

Red: Test model prediction interface

test("model predicts spam classification", () => {
  const model = new SpamClassifier(); // => FAILS: Model not defined
  const result = model.predict("Buy now! Limited offer!");
  expect(result.label).toBe("spam");
  expect(result.confidence).toBeGreaterThan(0.8);
});

Green: Test model interface (not internals)

interface Prediction {
  label: "spam" | "ham";
  confidence: number;
}

class SpamClassifier {
  predict(text: string): Prediction {
    // => Simplified model (real ML model would be complex)
    const spamKeywords = ["buy now", "limited offer", "click here"];
    const hasSpamKeyword = spamKeywords.some((keyword) => text.toLowerCase().includes(keyword));
    // => Checks for spam indicators

    return {
      label: hasSpamKeyword ? "spam" : "ham", // => Classification
      confidence: hasSpamKeyword ? 0.95 : 0.6, // => Confidence score
    };
  }
}

describe("SpamClassifier", () => {
  test("classifies obvious spam", () => {
    const model = new SpamClassifier();
    const result = model.predict("Buy now! Limited offer!");

    expect(result.label).toBe("spam"); // => Correct classification
    expect(result.confidence).toBeGreaterThan(0.8); // => High confidence
  });

  test("classifies normal email", () => {
    const model = new SpamClassifier();
    const result = model.predict("Meeting at 3pm today");

    expect(result.label).toBe("ham"); // => Correct classification
  });

  test("handles empty input", () => {
    const model = new SpamClassifier();
    const result = model.predict("");

    expect(result.label).toBe("ham"); // => Default classification
  });
});

Refactored: Test data pipeline and boundaries

describe("ML System Testing", () => {
  test("data preprocessing pipeline", () => {
    function preprocessText(text: string): string[] {
      // => Tokenization
      return text.toLowerCase().split(/\s+/); // => Split into words
    }

    const tokens = preprocessText("Buy Now Limited Offer");
    expect(tokens).toEqual(["buy", "now", "limited", "offer"]); // => Verify preprocessing
  });

  test("model prediction boundaries", () => {
    const model = new SpamClassifier();

    // Test known spam examples
    const spamExamples = ["Buy now!", "Click here for free", "Limited time offer"];

    spamExamples.forEach((example) => {
      const result = model.predict(example);
      expect(result.label).toBe("spam"); // => All should be spam
    });
  });
});

Key Takeaway: Test ML systems through interfaces, pipelines, and boundary cases. Don’t test model internals, but test preprocessing, prediction API, and edge cases.

Why It Matters: ML models are non-deterministic but pipelines are testable. Pipeline bugs cause model failures - ML testing with high coverage on data pipelines and model interfaces can prevent most production failures.

Example 79: Testing AI/ML Model Behavior

AI models require behavioral testing through example inputs and expected outputs. Test edge cases and model degradation.

Red: Test model behavior on edge cases

test("sentiment model handles sarcasm", () => {
  const model = new SentimentAnalyzer(); // => FAILS: Model not defined
  const result = model.analyze("Oh great, another Monday!");
  expect(result.sentiment).toBe("negative"); // Sarcasm detection
});

Green: Test behavioral boundaries

interface SentimentResult {
  sentiment: "positive" | "negative" | "neutral";
  score: number;
}

class SentimentAnalyzer {
  analyze(text: string): SentimentResult {
    // => Simplified model
    const positiveWords = ["great", "excellent", "amazing"];
    const negativeWords = ["terrible", "awful", "hate", "monday"];

    const hasNegative = negativeWords.some((word) => text.toLowerCase().includes(word));
    const hasPositive = positiveWords.some((word) => text.toLowerCase().includes(word));

    if (hasNegative && hasPositive) {
      // => Sarcasm indicator
      return { sentiment: "negative", score: 0.3 }; // => Assume sarcasm is negative
    } else if (hasNegative) {
      return { sentiment: "negative", score: 0.8 };
    } else if (hasPositive) {
      return { sentiment: "positive", score: 0.8 };
    }

    return { sentiment: "neutral", score: 0.5 }; // => Default neutral
  }
}

describe("SentimentAnalyzer behavior", () => {
  test("detects positive sentiment", () => {
    const model = new SentimentAnalyzer();
    const result = model.analyze("This is amazing!");
    expect(result.sentiment).toBe("positive");
  });

  test("detects negative sentiment", () => {
    const model = new SentimentAnalyzer();
    const result = model.analyze("This is terrible");
    expect(result.sentiment).toBe("negative");
  });

  test("handles sarcasm", () => {
    const model = new SentimentAnalyzer();
    const result = model.analyze("Oh great, another Monday!");
    expect(result.sentiment).toBe("negative"); // => Sarcasm interpreted as negative
  });

  test("handles neutral text", () => {
    const model = new SentimentAnalyzer();
    const result = model.analyze("The meeting is at 3pm");
    expect(result.sentiment).toBe("neutral");
  });
});

Key Takeaway: Test AI model behavior through input-output examples covering normal cases, edge cases, and boundary conditions. Focus on model interface, not internals.

Why It Matters: AI model behavior shifts over time. Behavioral tests catch regressions - OpenAI’s GPT testing uses thousands of behavioral examples to detect model degradation between versions.

Example 80: Evolutionary Architecture with TDD

Evolutionary architecture evolves through incremental changes guided by fitness functions. TDD provides fast feedback for architectural decisions.

Red: Test architectural constraint (fitness function)

test("modules do not have circular dependencies", () => {
  const dependencies = analyzeDependencies(); // => FAILS: Analysis not implemented
  const hasCycles = detectCycles(dependencies);
  expect(hasCycles).toBe(false); // Architectural constraint
});

Green: Implement fitness function

type DependencyGraph = Map<string, Set<string>>;

function analyzeDependencies(): DependencyGraph {
  // => Simplified dependency analysis
  const graph: DependencyGraph = new Map();
  graph.set("moduleA", new Set(["moduleB"])); // => A depends on B
  graph.set("moduleB", new Set(["moduleC"])); // => B depends on C
  graph.set("moduleC", new Set()); // => C has no dependencies
  return graph;
}

function detectCycles(graph: DependencyGraph): boolean {
  const visited = new Set<string>();
  const recursionStack = new Set<string>();

  function hasCycle(node: string): boolean {
    if (recursionStack.has(node)) return true; // => Cycle detected
    if (visited.has(node)) return false; // => Already checked

    visited.add(node);
    recursionStack.add(node);

    const neighbors = graph.get(node) || new Set();
    for (const neighbor of neighbors) {
      if (hasCycle(neighbor)) return true;
    }

    recursionStack.delete(node);
    return false;
  }

  for (const node of graph.keys()) {
    if (hasCycle(node)) return true;
  }

  return false; // => No cycles found
}

describe("Evolutionary Architecture Fitness Functions", () => {
  test("detects no cycles in valid dependency graph", () => {
    const dependencies = analyzeDependencies();
    const hasCycles = detectCycles(dependencies);
    expect(hasCycles).toBe(false); // => Valid architecture
  });

  test("detects cycles in invalid dependency graph", () => {
    const graph: DependencyGraph = new Map();
    graph.set("A", new Set(["B"]));
    graph.set("B", new Set(["C"]));
    graph.set("C", new Set(["A"])); // => Cycle: A → B → C → A

    const hasCycles = detectCycles(graph);
    expect(hasCycles).toBe(true); // => Cycle detected
  });
});

Key Takeaway: Evolutionary architecture uses fitness functions (automated tests) to verify architectural constraints. TDD fitness functions prevent architectural degradation.

Why It Matters: Architecture erodes without automated checks. Fitness functions enable safe evolution - Evolutionary architecture practices use automated fitness functions to maintain architectural integrity.

Example 81: TDD in Continuous Deployment

Continuous deployment requires high confidence in automated tests. TDD enables safe deployment to production multiple times per day.

Challenge: Deploy to production safely without manual testing

Solution: Comprehensive test pyramid + automated deployment

// Test Pyramid Structure
describe("Test Pyramid for Continuous Deployment", () => {
  // Layer 1: Unit Tests (70% of tests) - Fast, isolated
  describe("Unit Tests", () => {
    test("business logic is correct", () => {
      const calculator = new PriceCalculator();
      expect(calculator.calculateDiscount(100, 0.1)).toBe(90);
    }); // => Millisecond execution

    // 1000+ unit tests covering all business logic
  });

  // Layer 2: Integration Tests (20% of tests) - Medium speed
  describe("Integration Tests", () => {
    test("service integrates with database", async () => {
      const db = new InMemoryDatabase(); // => Fake database
      const service = new UserService(db);

      await service.createUser("alice@example.com", { name: "Alice" });
      const user = await service.getUser("alice@example.com");

      expect(user?.name).toBe("Alice");
    }); // => Sub-second execution

    // 200+ integration tests covering component interactions
  });

  // Layer 3: End-to-End Tests (10% of tests) - Slow but comprehensive
  describe("E2E Tests", () => {
    test("user can complete checkout flow", async () => {
      const browser = await launchBrowser();
      await browser.goto("/products");
      await browser.click("[data-testid=add-to-cart]");
      await browser.click("[data-testid=checkout]");
      const confirmation = await browser.text("[data-testid=order-confirmation]");

      expect(confirmation).toContain("Order confirmed");
      await browser.close();
    }); // => Seconds to minutes

    // 50+ E2E tests covering critical user flows
  });
});

// Deployment Pipeline
test("deployment pipeline validates quality gates", () => {
  const pipeline = {
    stages: [
      { name: "Unit Tests", threshold: 95, coverage: 96, status: "pass" }, // => 95% required
      { name: "Integration Tests", threshold: 85, coverage: 88, status: "pass" }, // => 85% required
      { name: "E2E Tests", threshold: 100, passed: 50, total: 50, status: "pass" }, // => All must pass
      { name: "Deploy to Production", status: "ready" }, // => Automated deployment
    ],
  };

  const allStagesPassed = pipeline.stages.slice(0, -1).every((stage) => stage.status === "pass");
  expect(allStagesPassed).toBe(true); // => Quality gates met
}); // => Deploy with confidence

Key Metrics for CD:

interface CDMetrics {
  deploymentFrequency: string; // => "10+ per day"
  leadTime: string; // => "< 1 hour (commit to production)"
  mttr: string; // => "< 15 minutes (mean time to recovery)"
  changeFailureRate: number; // => "< 5% (failed deployments)"
}

const cdMetrics: CDMetrics = {
  deploymentFrequency: "10+ per day", // => High deployment frequency
  leadTime: "< 1 hour", // => Fast feedback
  mttr: "< 15 minutes", // => Quick recovery
  changeFailureRate: 0.03, // => 3% failure rate (97% success)
};

test("CD metrics meet industry benchmarks", () => {
  expect(cdMetrics.changeFailureRate).toBeLessThan(0.05); // => Less than 5%
}); // => Automated testing enables safe CD

Key Takeaway: Continuous deployment requires comprehensive test automation at all levels (unit, integration, E2E). TDD builds this test coverage from the start.

Why It Matters: Manual testing blocks frequent deployment. High-frequency deployment systems achieve very rapid deployment cycles using automated testing - TDD discipline enables hundreds of thousands of safe deployments annually with high success rates.

Example 82: Production Testing Patterns

Production testing catches issues that pre-production testing misses. Use canary deployments, feature flags, and synthetic monitoring.

Red: Test canary deployment

test("canary deployment serves 5% of traffic", () => {
  const deployment = new CanaryDeployment(); // => FAILS: Not implemented
  deployment.deploy("v2.0", { canaryPercentage: 5 });

  const trafficDistribution = deployment.getTrafficDistribution();
  expect(trafficDistribution.v1).toBe(95);
  expect(trafficDistribution.v2).toBe(5);
});

Green: Canary deployment implementation

interface DeploymentConfig {
  canaryPercentage: number;
}

class CanaryDeployment {
  private currentVersion = "v1.0";
  private canaryVersion: string | null = null;
  private canaryPercentage = 0;

  deploy(version: string, config: DeploymentConfig): void {
    this.canaryVersion = version; // => New version
    this.canaryPercentage = config.canaryPercentage; // => Traffic percentage
  }

  routeRequest(requestId: number): string {
    // => Deterministic routing based on request ID
    const isCanary = requestId % 100 < this.canaryPercentage;
    return isCanary && this.canaryVersion ? this.canaryVersion : this.currentVersion;
  }

  getTrafficDistribution(): Record<string, number> {
    const distribution: Record<string, number> = {};
    distribution[this.currentVersion] = 100 - this.canaryPercentage;
    if (this.canaryVersion) {
      distribution[this.canaryVersion] = this.canaryPercentage;
    }
    return distribution;
  }
}

describe("Canary Deployment", () => {
  test("routes 5% traffic to canary", () => {
    const deployment = new CanaryDeployment();
    deployment.deploy("v2.0", { canaryPercentage: 5 });

    const distribution = deployment.getTrafficDistribution();
    expect(distribution["v1.0"]).toBe(95); // => 95% to stable
    expect(distribution["v2.0"]).toBe(5); // => 5% to canary
  });

  test("routes requests correctly", () => {
    const deployment = new CanaryDeployment();
    deployment.deploy("v2.0", { canaryPercentage: 5 });

    const results = Array.from({ length: 100 }, (_, i) => deployment.routeRequest(i));
    const canaryCount = results.filter((v) => v === "v2.0").length;

    expect(canaryCount).toBe(5); // => Exactly 5% routed to canary
  });
});

Key Takeaway: Test production deployment patterns (canary, blue-green, feature flags) to verify gradual rollout and quick rollback capabilities.

Why It Matters: Production has unique conditions impossible to replicate in staging. Canary testing enables safe production validation - Canary rollouts can catch critical bugs affecting minimal users.

Example 83: Testing with Feature Flags

Feature flags enable testing new features in production with controlled rollout. TDD tests flag behavior and gradual enablement.

Red: Test feature flag behavior

test("feature flag controls new checkout flow", () => {
  const flags = new FeatureFlags(); // => FAILS: Not implemented
  flags.enable("new-checkout", { percentage: 10 });

  const isEnabled = flags.isEnabled("new-checkout", "user-123");
  expect(typeof isEnabled).toBe("boolean"); // Should return true or false
});

Green: Feature flag implementation

interface FlagConfig {
  percentage?: number;
  users?: string[];
}

class FeatureFlags {
  private flags: Map<string, FlagConfig> = new Map();

  enable(flagName: string, config: FlagConfig = {}): void {
    this.flags.set(flagName, config); // => Enable flag with config
  }

  isEnabled(flagName: string, userId: string): boolean {
    const config = this.flags.get(flagName);
    if (!config) return false; // => Flag not enabled

    // Check user whitelist
    if (config.users?.includes(userId)) return true;

    // Check percentage rollout
    if (config.percentage) {
      const hash = this.hashUserId(userId); // => Deterministic hash
      return hash % 100 < config.percentage; // => Percentage-based rollout
    }

    return false;
  }

  private hashUserId(userId: string): number {
    // => Simple hash function
    let hash = 0;
    for (let i = 0; i < userId.length; i++) {
      hash = (hash << 5) - hash + userId.charCodeAt(i);
      hash = hash & hash; // => Convert to 32-bit integer
    }
    return Math.abs(hash);
  }
}

describe("Feature Flags", () => {
  test("enables feature for whitelisted users", () => {
    const flags = new FeatureFlags();
    flags.enable("new-checkout", { users: ["user-123", "user-456"] });

    expect(flags.isEnabled("new-checkout", "user-123")).toBe(true); // => Whitelisted
    expect(flags.isEnabled("new-checkout", "user-789")).toBe(false); // => Not whitelisted
  });

  test("enables feature for percentage of users", () => {
    const flags = new FeatureFlags();
    flags.enable("new-checkout", { percentage: 10 });

    const results = Array.from({ length: 1000 }, (_, i) => flags.isEnabled("new-checkout", `user-${i}`));
    const enabledCount = results.filter((enabled) => enabled).length;

    expect(enabledCount).toBeGreaterThan(50); // => Approximately 10% (100)
    expect(enabledCount).toBeLessThan(150); // => Within reasonable variance
  });

  test("consistent rollout for same user", () => {
    const flags = new FeatureFlags();
    flags.enable("new-checkout", { percentage: 50 });

    const result1 = flags.isEnabled("new-checkout", "user-123");
    const result2 = flags.isEnabled("new-checkout", "user-123");

    expect(result1).toBe(result2); // => Deterministic for same user
  });
});

Key Takeaway: Test feature flags for whitelist behavior, percentage rollout, and deterministic assignment. Flags enable production testing without full deployment.

Why It Matters: Feature flags enable safe experimentation in production. TDD ensures flag logic works correctly - Etsy runs thousands of A/B experiments simultaneously using tested feature flags.

Example 84: Synthetic Monitoring and Production Tests

Synthetic monitoring runs automated tests against production to detect issues before users. Combine with alerting for fast incident response.

Red: Test production endpoint health

test("production API responds within SLA", async () => {
  const monitor = new SyntheticMonitor(); // => FAILS: Not implemented
  const result = await monitor.checkEndpoint("/api/users");
  expect(result.responseTime).toBeLessThan(500); // < 500ms SLA
});

Green: Synthetic monitoring implementation

interface MonitorResult {
  endpoint: string;
  status: number;
  responseTime: number;
  success: boolean;
}

class SyntheticMonitor {
  async checkEndpoint(endpoint: string, baseUrl = "https://api.example.com"): Promise<MonitorResult> {
    const startTime = Date.now();

    try {
      const response = await fetch(`${baseUrl}${endpoint}`); // => Call production
      const responseTime = Date.now() - startTime; // => Measure latency

      return {
        endpoint,
        status: response.status,
        responseTime,
        success: response.ok, // => 2xx status
      };
    } catch (error) {
      return {
        endpoint,
        status: 0,
        responseTime: Date.now() - startTime,
        success: false,
      };
    }
  }
}

describe("Synthetic Monitoring", () => {
  test("monitors production endpoint health", async () => {
    const monitor = new SyntheticMonitor();
    const result = await monitor.checkEndpoint("/api/health");

    expect(result.success).toBe(true); // => Endpoint healthy
    expect(result.status).toBe(200); // => 200 OK
    expect(result.responseTime).toBeLessThan(500); // => Under 500ms SLA
  });

  test("detects endpoint failure", async () => {
    const monitor = new SyntheticMonitor();
    const result = await monitor.checkEndpoint("/api/nonexistent");

    expect(result.success).toBe(false); // => Endpoint failed
    expect(result.status).toBe(404); // => 404 Not Found
  });
});

// Continuous monitoring (runs every minute)
setInterval(async () => {
  const monitor = new SyntheticMonitor();
  const result = await monitor.checkEndpoint("/api/critical");

  if (!result.success || result.responseTime > 1000) {
    console.error("ALERT: Production endpoint degraded", result);
    // Trigger PagerDuty/Slack alert
  }
}, 60000); // => Every 60 seconds

Key Takeaway: Synthetic monitoring runs automated tests against production continuously. Alerts on SLA violations or failures enable fast incident response.

Why It Matters: Production issues affect users immediately. Synthetic monitoring detects problems before users report them - Datadog’s synthetic monitoring users detect 80% of production issues through automated tests rather than customer complaints.

Example 85: TDD Anti-Pattern Recovery - Abandoned Test Suites

Test suites decay when ignored. Revive abandoned tests through cleanup, deprecation, and reintroduction of TDD discipline.

Problem: Abandoned test suite

describe("Abandoned Test Suite", () => {
  test.skip("this test is flaky"); // => Skipped instead of fixed
  test.skip("TODO: Fix this later"); // => Never fixed
  test("test with no assertions", () => {}); // => Empty test
  test("outdated test for removed feature", () => {
    // => Tests nonexistent code
    const feature = oldFeature();
    expect(feature).toBeDefined();
  });
});

Solution: Test suite revival

describe("Test Suite Revival Process", () => {
  // Step 1: Remove dead tests
  // ❌ DELETE: Tests for removed features
  // ❌ DELETE: Empty tests with no assertions
  // ❌ DELETE: Tests that never pass

  // Step 2: Fix or delete flaky tests
  test("previously flaky test - now fixed with proper async handling", async () => {
    jest.useFakeTimers(); // => Control time
    const callback = jest.fn();
    setTimeout(callback, 1000);

    jest.advanceTimersByTime(1000); // => Deterministic timing
    expect(callback).toHaveBeenCalled(); // => No longer flaky

    jest.useRealTimers();
  });

  // Step 3: Re-enable tests with proper fixes
  test("user creation saves to database", async () => {
    const db = new InMemoryDatabase(); // => Fast fake
    const service = new UserService(db);

    await service.createUser({ name: "Alice" });
    const users = await db.getAll();

    expect(users).toHaveLength(1); // => Proper assertion
    expect(users[0].name).toBe("Alice");
  });

  // Step 4: Add missing tests for critical paths
  test("checkout flow processes payment", async () => {
    const mockPayment = { charge: jest.fn().mockResolvedValue(true) };
    const checkout = new CheckoutService(mockPayment);

    const result = await checkout.processOrder({ total: 100 });

    expect(result.success).toBe(true);
    expect(mockPayment.charge).toHaveBeenCalledWith(100);
  }); // => Previously missing test
});

// Step 5: Enforce quality gates
describe("Quality Gates", () => {
  test("coverage meets minimum threshold", () => {
    const coverageReport = {
      lines: { pct: 85 },
      branches: { pct: 82 },
      functions: { pct: 88 },
    };

    Object.values(coverageReport).forEach((metric) => {
      expect(metric.pct).toBeGreaterThan(80); // => 80% minimum
    });
  }); // => Enforced in CI

  test("no skipped tests allowed", () => {
    const skippedTests = 0; // => Count from test runner
    expect(skippedTests).toBe(0); // => All tests must run
  });
});

Revival Checklist:

- [ ] Delete tests for removed features (dead code)
- [ ] Delete empty tests with no assertions
- [ ] Fix flaky tests with proper async handling
- [ ] Unskip tests and fix properly (no test.skip allowed)
- [ ] Add tests for critical untested paths
- [ ] Enforce coverage thresholds (80% minimum)
- [ ] Enable pre-commit hooks to prevent test decay
- [ ] Document testing standards for team
- [ ] Schedule regular test health reviews

Key Takeaway: Revive abandoned test suites by removing dead tests, fixing flaky tests, and enforcing quality gates. Prevention through CI enforcement and team discipline.

Why It Matters: Abandoned test suites provide false security. Active test maintenance preserves value - Research indicates that


This completes the advanced level (Examples 59-85) covering enterprise TDD patterns including legacy code testing, approval testing, distributed systems, microservices, machine learning, evolutionary architecture, continuous deployment, and production testing patterns. Total coverage: 75-95% of TDD practices needed for professional software development.

Skill Mastery: Developers completing this advanced section can apply TDD in complex enterprise environments, test legacy systems, scale TDD across teams, and implement production testing strategies with confidence.

Last updated