TDD Schools
There are two major approaches to TDD: the London School (Mockist) and the Chicago School (Classical). Understanding both helps you choose the right approach for each situation.
Overview
| Aspect | London School | Chicago School |
|---|---|---|
| Also Known As | Mockist, Outside-In | Classical, Detroit, Inside-Out |
| Primary Focus | Behavior & Collaboration | State & Results |
| Test Doubles | Heavy use of mocks | Minimal, prefer real objects |
| Design Driver | Interactions between objects | Data transformations |
| Starting Point | Outer layer (controllers) | Inner layer (domain) |
Historical Background
Are These Real Schools?
The terms "London School" and "Chicago School" are not formal institutions - they're community-adopted names for two distinct TDD approaches that emerged from different practitioner groups.
Chicago School (Classical TDD)
| Origin | Emerged from the Extreme Programming (XP) community in the late 1990s |
| Location | Named after Kent Beck's work at Chrysler in Detroit (often called "Detroit School" too) |
| Key Figures | Kent Beck (creator of TDD and XP), Ward Cunningham, Ron Jeffries |
| Seminal Work | Test-Driven Development: By Example (2002) by Kent Beck |
| First Practices | C3 Project at Chrysler (1996-1999) |
Kent Beck formalized TDD while working on the C3 payroll project. The approach emphasizes simplicity, real objects, and state verification - "fake it till you make it."
London School (Mockist TDD)
| Origin | Developed by practitioners in London's XP/Agile community, early 2000s |
| Location | Named after the London-based consultancy where key ideas formed |
| Key Figures | Steve Freeman, Nat Pryce, Tim Mackinnon, Joe Walnes |
| Seminal Work | Growing Object-Oriented Software, Guided by Tests (2009) |
| Key Innovation | jMock framework (2004) - first mock object library |
The London practitioners found that mocking collaborators led to better interface design. Their "tell, don't ask" philosophy promotes behavior verification over state checking.
The Great Debate
The distinction became prominent after Martin Fowler's influential article Mocks Aren't Stubs (2007), which clarified the differences between the two approaches and their philosophies.
Neither is "Better"
Both schools have produced successful software. The "right" choice depends on context, team preferences, and the problem at hand. Most modern practitioners use a hybrid approach.
London School (Mockist)
The London School emphasizes behavior verification and outside-in development. Tests focus on how objects collaborate rather than their internal state.
Key Principles
- Mock collaborators - Replace dependencies with test doubles
- Verify interactions - Assert that methods were called correctly
- Outside-in - Start from acceptance test, work inward
- One object at a time - Isolate the unit under test
Example: London Style
test('processes order by charging payment and sending confirmation', function () {
// Arrange - Mock collaborators
$paymentGateway = Mockery::mock(PaymentGateway::class);
$emailService = Mockery::mock(EmailService::class);
$order = new Order(amount: 100);
// Expect interactions
$paymentGateway->shouldReceive('charge')
->once()
->with($order->amount)
->andReturn(true);
$emailService->shouldReceive('sendConfirmation')
->once()
->with($order);
// Act
$processor = new OrderProcessor($paymentGateway, $emailService);
$processor->process($order);
// Assert - Mockery verifies expectations automatically
});When to Use London Style
- Complex collaborations - Many services interacting
- External dependencies - APIs, databases, file systems
- Behavior is important - The "how" matters as much as "what"
- Design exploration - Discovering interfaces through tests
Pros and Cons
| Pros | Cons |
|---|---|
| Tests run fast (no real dependencies) | Tests coupled to implementation |
| Forces clear interfaces | Refactoring can break tests |
| Isolated failures | Can miss integration issues |
| Drives emergent design | More setup code |
Chicago School (Classical)
The Chicago School emphasizes state verification and uses real objects whenever possible. Tests focus on inputs and outputs rather than internal behavior.
Key Principles
- Real objects preferred - Only mock external boundaries
- Verify state - Assert on the result, not the journey
- Inside-out - Build from the domain outward
- Test behavior through state - The output proves the behavior
Example: Chicago Style
test('processes order and updates status', function () {
// Arrange - Use real objects
$paymentGateway = new FakePaymentGateway(); // In-memory fake
$emailService = new FakeEmailService(); // Collects sent emails
$order = new Order(amount: 100);
// Act
$processor = new OrderProcessor($paymentGateway, $emailService);
$processor->process($order);
// Assert - Check state
expect($order->status)->toBe('processed');
expect($paymentGateway->charges)->toHaveCount(1);
expect($emailService->sentEmails)->toHaveCount(1);
});When to Use Chicago Style
- Business logic - Domain rules and calculations
- Data transformations - Input → Output functions
- Stable interfaces - When APIs won't change
- Integration confidence - Want to test real interactions
Pros and Cons
| Pros | Cons |
|---|---|
| Tests survive refactoring | Can be slower |
| Tests real behavior | Harder to isolate failures |
| Less mock ceremony | May need test infrastructure |
| Catches integration bugs | Tests can be more complex |
Double Loop with Both Schools
The Double Loop naturally combines both schools:
┌─────────────────────────────────────────────────────────────┐
│ OUTER LOOP (BDD) │
│ Chicago Style - Real Integration │
│ │
│ Feature file → Step definitions → Real services │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ INNER LOOP (TDD) │ │
│ │ London Style - Isolated Units │ │
│ │ │ │
│ │ Unit test → Mock collaborators → Single class │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Practical Combination
Outer Loop (BDD) - Chicago Style:
- Feature tests use real objects
- Test the full stack integration
- Verify user-visible behavior
Inner Loop (TDD) - London Style:
- Unit tests mock dependencies
- Focus on single class behavior
- Fast feedback during development
Example: Combined Approach
# Outer Loop - Chicago (real integration)
Feature: Order Processing
Scenario: Complete order
Given a customer with balance $500
When they place an order for $100
Then the order should be processed
And the customer balance should be $400// Inner Loop - London (mocked units)
test('order processor charges payment gateway', function () {
$gateway = Mockery::mock(PaymentGateway::class);
$gateway->shouldReceive('charge')->once()->with(100);
$processor = new OrderProcessor($gateway);
$processor->process(new Order(100));
});
test('order processor sends confirmation email', function () {
$mailer = Mockery::mock(Mailer::class);
$mailer->shouldReceive('send')->once();
$processor = new OrderProcessor(
new FakePaymentGateway(),
$mailer
);
$processor->process(new Order(100));
});Choosing Your Approach
Use London When:
// Complex service with many dependencies
class OrderProcessor
{
public function __construct(
private PaymentGateway $payments,
private InventoryService $inventory,
private EmailService $email,
private AuditLogger $audit,
) {}
}
// → Mock each dependency, test interactionsUse Chicago When:
// Pure business logic
class PriceCalculator
{
public function calculate(Cart $cart, Discount $discount): Money
{
// Complex calculation logic
}
}
// → Use real objects, test outputHybrid Approach
Most projects benefit from both:
| Layer | Approach | Reason |
|---|---|---|
| Controllers | London | Test HTTP handling in isolation |
| Services | London/Chicago | Depends on complexity |
| Domain Models | Chicago | Test business rules with real objects |
| Repositories | Chicago | Test with in-memory implementations |
| External APIs | London | Always mock external boundaries |
Test Double Types
Both schools use test doubles, but differently:
| Type | Description | London | Chicago |
|---|---|---|---|
| Mock | Verifies interactions | Heavy use | Rare |
| Stub | Returns canned responses | Common | Common |
| Fake | Working implementation | Rare | Heavy use |
| Spy | Records calls for later verification | Common | Occasional |
| Dummy | Placeholder, never used | Both | Both |
Examples
// Mock - Verifies behavior (London)
$mock = Mockery::mock(Gateway::class);
$mock->shouldReceive('charge')->once();
// Stub - Provides data (Both)
$stub = Mockery::mock(UserRepository::class);
$stub->shouldReceive('find')->andReturn(new User());
// Fake - Real implementation (Chicago)
class FakePaymentGateway implements PaymentGateway
{
public array $charges = [];
public function charge(int $amount): bool
{
$this->charges[] = $amount;
return true;
}
}
// Spy - Records for later (Both)
$spy = Mockery::spy(Logger::class);
$service->doSomething();
$spy->shouldHaveReceived('log');Recommendations
For Pest BDD Projects
Outer Loop (BDD): Use Chicago style
- Real factories create real models
- Steps execute real code paths
- Assertions check real database state
Inner Loop (TDD): Mix based on context
- Services with many dependencies → London
- Domain logic and calculations → Chicago
- External API calls → Always mock
Step Definitions: Keep them thin
- Steps should delegate to real code
- Don't put business logic in steps
- Let the production code be tested
Example Project Structure
tests/
├── Behaviors/ # BDD - Chicago style
│ └── checkout.feature # Real integration
├── Unit/
│ ├── Services/ # TDD - London style
│ │ └── OrderProcessorTest.php
│ └── Domain/ # TDD - Chicago style
│ └── PriceCalculatorTest.php
└── Integration/ # Chicago style
└── CheckoutFlowTest.phpFurther Reading
- Growing Object-Oriented Software, Guided by Tests - London School bible
- Test-Driven Development: By Example - Kent Beck, Chicago School
- Mocks Aren't Stubs - Martin Fowler's comparison