Skip to content

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

AspectLondon SchoolChicago School
Also Known AsMockist, Outside-InClassical, Detroit, Inside-Out
Primary FocusBehavior & CollaborationState & Results
Test DoublesHeavy use of mocksMinimal, prefer real objects
Design DriverInteractions between objectsData transformations
Starting PointOuter 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)

OriginEmerged from the Extreme Programming (XP) community in the late 1990s
LocationNamed after Kent Beck's work at Chrysler in Detroit (often called "Detroit School" too)
Key FiguresKent Beck (creator of TDD and XP), Ward Cunningham, Ron Jeffries
Seminal WorkTest-Driven Development: By Example (2002) by Kent Beck
First PracticesC3 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)

OriginDeveloped by practitioners in London's XP/Agile community, early 2000s
LocationNamed after the London-based consultancy where key ideas formed
Key FiguresSteve Freeman, Nat Pryce, Tim Mackinnon, Joe Walnes
Seminal WorkGrowing Object-Oriented Software, Guided by Tests (2009)
Key InnovationjMock 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

  1. Mock collaborators - Replace dependencies with test doubles
  2. Verify interactions - Assert that methods were called correctly
  3. Outside-in - Start from acceptance test, work inward
  4. One object at a time - Isolate the unit under test

Example: London Style

php
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

ProsCons
Tests run fast (no real dependencies)Tests coupled to implementation
Forces clear interfacesRefactoring can break tests
Isolated failuresCan miss integration issues
Drives emergent designMore 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

  1. Real objects preferred - Only mock external boundaries
  2. Verify state - Assert on the result, not the journey
  3. Inside-out - Build from the domain outward
  4. Test behavior through state - The output proves the behavior

Example: Chicago Style

php
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

ProsCons
Tests survive refactoringCan be slower
Tests real behaviorHarder to isolate failures
Less mock ceremonyMay need test infrastructure
Catches integration bugsTests 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

gherkin
# 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
php
// 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:

php
// 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 interactions

Use Chicago When:

php
// Pure business logic
class PriceCalculator
{
    public function calculate(Cart $cart, Discount $discount): Money
    {
        // Complex calculation logic
    }
}
// → Use real objects, test output

Hybrid Approach

Most projects benefit from both:

LayerApproachReason
ControllersLondonTest HTTP handling in isolation
ServicesLondon/ChicagoDepends on complexity
Domain ModelsChicagoTest business rules with real objects
RepositoriesChicagoTest with in-memory implementations
External APIsLondonAlways mock external boundaries

Test Double Types

Both schools use test doubles, but differently:

TypeDescriptionLondonChicago
MockVerifies interactionsHeavy useRare
StubReturns canned responsesCommonCommon
FakeWorking implementationRareHeavy use
SpyRecords calls for later verificationCommonOccasional
DummyPlaceholder, never usedBothBoth

Examples

php
// 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

  1. Outer Loop (BDD): Use Chicago style

    • Real factories create real models
    • Steps execute real code paths
    • Assertions check real database state
  2. Inner Loop (TDD): Mix based on context

    • Services with many dependencies → London
    • Domain logic and calculations → Chicago
    • External API calls → Always mock
  3. 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.php

Further Reading

Released under the MIT License.