London vs Detroit Testing Styles
Introduction#
Every software team debates how to write effective tests, but most discussions miss the fundamental philosophical divide that shapes every testing decision. Two schools of thought have emerged from the Test-Driven Development community, each answering a different question about what tests should verify.
Understanding these two approaches, London and Detroit testing styles, helps you make intentional choices about your testing strategy rather than accidentally mixing incompatible patterns that create maintenance headaches.
The Fundamental Question#
The difference between London and Detroit testing comes down to a simple question: what should your tests verify?
Detroit asks: Does the system produce the correct result?
London asks: Does the system communicate correctly?
This philosophical difference cascades into every aspect of how you write, structure, and maintain your tests. Detroit focuses on final outcomes, while London focuses on interactions between components.
Neither approach is inherently better. They solve different problems and make different trade-offs. The key is understanding when each approach serves your needs.
Detroit Style: Testing Outcomes#
Detroit testing treats your code like a black box. You provide inputs and verify outputs, without caring about the internal mechanics. This approach emerged from Kent Beck’s work on the Chrysler C3 project in Detroit during the 1990s.
How Detroit Works#
Detroit defines a “unit” as any cohesive module that provides meaningful functionality. This could be a single class, or it could be dozens of classes working together. The size depends on what makes sense as a complete, testable behavior.
You use real implementations for all dependencies except external systems like databases or APIs. If your payment processor calls a receipt generator, which calls a tax calculator, Detroit tests use the real receipt generator and tax calculator instances.
// Detroit approach - uses real dependencies
describe('PaymentProcessor', () => {
it('processes payment correctly', () => {
const realReceiptGenerator = new ReceiptGenerator();
const realTaxCalculator = new TaxCalculator();
const processor = new PaymentProcessor(
realReceiptGenerator,
realTaxCalculator
);
const result = processor.processPayment({
amount: 100,
customerId: 'cust-123'
});
expect(result.total).toBe(108.25); // verify final state
expect(result.receipt).toContain('cust-123');
});
});
Detroit Strengths#
Detroit tests survive refactoring beautifully. You can completely restructure your internal implementation, extract new classes, or change how components communicate, and your tests continue passing as long as the final behavior remains correct.
These tests also give you confidence that your components actually work together. Since you’re using real implementations, you catch integration problems immediately rather than discovering them later when components meet for the first time.
Detroit Challenges#
Setup complexity grows quickly with deep dependency chains. Testing a payment processor might require configuring a receipt generator, tax calculator, currency converter, and validation service. Each dependency needs proper initialization and state management.
When tests fail, pinpointing the root cause becomes detective work. A bug in the shared tax calculator might break payment processor tests, order processor tests, and invoice generator tests simultaneously.
London Style: Testing Interactions#
London testing treats each class as an isolated unit and replaces all collaborators with test doubles. This approach emerged from the Extreme Tuesday Club in London during the early 2000s and was formalized by Steve Freeman and Nat Pryce.
How London Works#
London defines a “unit” strictly as a single class. Every dependency gets replaced with a mock or stub that you control completely. Your tests verify that the class under test makes the correct method calls with the expected parameters.
// London approach - mocks all dependencies
describe('PaymentProcessor', () => {
it('calls receipt generator with correct data', () => {
const mockReceiptGenerator = {
generate: jest.fn().mockReturnValue({ id: 'receipt-123' })
};
const mockTaxCalculator = {
calculate: jest.fn().mockReturnValue(8.25)
};
const processor = new PaymentProcessor(
mockReceiptGenerator,
mockTaxCalculator
);
processor.processPayment({ amount: 100, customerId: 'cust-123' });
// verify interactions, not final state
expect(mockTaxCalculator.calculate).toHaveBeenCalledWith(100);
expect(mockReceiptGenerator.generate).toHaveBeenCalledWith({
amount: 100,
tax: 8.25,
customerId: 'cust-123'
});
});
});
London Strengths#
London provides surgical precision when tests fail. Since each test only exercises one class, a failing test points directly to the problematic component. No detective work required.
Testing complex scenarios becomes manageable because you control all dependencies through mocks. Need to test how your payment processor handles a tax calculation error? Just make the mock tax calculator throw an exception.
London also enables outside-in development. You can write tests for your API controllers before the domain services exist, using mocks to define the interfaces you need.
Design Benefits#
London testing acts as a design tool that forces you to think about interfaces and dependencies upfront. When you must mock every collaborator, you quickly discover tight coupling and complex dependencies that would otherwise hide in your implementation.
The need to create mocks reveals interface design problems early. If a mock is difficult to set up or requires extensive configuration, it often signals that the real interface is too complex or poorly designed. This feedback helps you create cleaner, more focused interfaces before writing the implementation.
London testing also encourages the Command-Query Separation principle. Methods that return values are easy to stub, while methods that cause side effects are easy to verify through mock expectations. This natural separation leads to more predictable and testable code.
London Challenges#
Tests become brittle during refactoring because they’re coupled to implementation details. Change how your payment processor calls the tax calculator, and you must update the interaction expectations in your test.
Mock drift creates a dangerous illusion of safety. Your mocks might behave differently than the real implementations, causing tests to pass while production code fails. You need separate integration tests to verify that your mocks match reality.
Choosing Your Approach#
Most successful teams don’t choose one style exclusively. Instead, they understand the trade-offs and apply each approach where it provides the most value.
Use Detroit When#
Your code changes frequently and you refactor often. Detroit tests provide a safety net that survives architectural changes, making them ideal for application development where requirements evolve rapidly.
You need confidence that your components work together correctly. Detroit’s use of real implementations catches integration bugs early, reducing surprises during system testing.
Use London When#
You’re building stable APIs or libraries where interfaces change infrequently. London’s focus on contracts works well when those contracts need to remain stable over time.
You need to test components that don’t exist yet or have complex dependency graphs. London’s mocking capabilities let you test in isolation and develop components in parallel.
Precise failure isolation matters more than integration confidence. In complex systems where pinpointing failures saves significant debugging time, London’s surgical precision pays dividends.
The Hybrid Approach#
Many teams apply both styles strategically. Use London for unit tests to isolate component behavior, then use Detroit for integration tests to verify collaboration. Use London when you need to design interfaces before implementations exist, then use Detroit to verify those interfaces work correctly together once implementations are built.
Consider your component types too. Pure logic and value objects work well with Detroit testing, while components with side effects often benefit from London’s mocking capabilities.
Practical Guidelines#
Start by identifying what you’re really trying to achieve with your tests. If you need to verify that your business logic produces correct results, Detroit’s outcome-focused approach serves you better. If you need to ensure components communicate properly, London’s interaction-focused approach provides better feedback.
Pay attention to your pain points. If you spend too much time updating tests during refactoring, consider moving toward Detroit style. If you struggle to isolate test failures in complex systems, London style might provide the precision you need.
Remember that these styles represent ends of a spectrum, not rigid categories. You can write tests that verify both interactions and outcomes, or use selective mocking where only some dependencies get replaced with test doubles.
The goal isn’t to pick the “right” style but to make intentional choices that align with your context, constraints, and team preferences. Both approaches have created successful, maintainable test suites when applied thoughtfully.
Conclusion#
London and Detroit testing styles represent different philosophies about what makes tests valuable. Detroit prioritizes integration confidence and refactoring safety by testing real component interactions. London prioritizes failure isolation and design feedback by testing component contracts in isolation.
Understanding these trade-offs lets you make intentional decisions about your testing strategy. You might use London for components with complex dependencies and Detroit for core business logic. You might start with London during rapid prototyping and transition to Detroit as your architecture stabilizes.
The worst outcome is accidentally mixing these approaches without understanding their implications, creating tests that are both brittle and hard to debug. The best outcome is deliberately choosing the approach that serves your specific context and goals.
Neither style will solve all your testing challenges, but both provide proven patterns for creating maintainable, valuable test suites when applied with intention and understanding.