--- title: "Mocking Strategies: Test Doubles, London vs Chicago Schools, and Avoiding Over-Mocking" description: "Precise definitions of dummies, stubs, fakes, spies, and mocks, the London (mockist) vs Chicago (classicist) schools of TDD, and concrete guidance on when mocking makes tests brittle and how to avoid over-mocking." language: null framework: null category: testing_general tags: - mocking - test-doubles - tdd - unit-testing - stubs - fakes - best-practices keywords: - mocks vs stubs vs fakes vs spies vs dummies - london school vs chicago school tdd - mockist vs classicist testing - over mocking brittle tests - unittest mock pytest-mock best practices last_updated: 2026-07-07 difficulty: intermediate version: "Testing theory; examples in Python (unittest.mock, pytest-mock)" related: - unit_testing_principles.md - integration_testing.md - ../architecture/hexagonal.md - ../architecture/clean_architecture.md - ../principles/solid.md - ../../languages/python/frameworks/fastapi/best_practices.md search_priority: high status: published --- # Mocking Strategies: Test Doubles, London vs Chicago Schools, and Avoiding Over-Mocking ## Introduction / Overview "Mock" is routinely used as a catch-all verb ("I'll just mock the database") for what is actually five distinct techniques with different purposes and different failure modes. Martin Fowler's *Mocks Aren't Stubs* (2007), still the canonical reference in 2026, formalized the vocabulary this document uses: **dummy**, **stub**, **spy**, **mock**, and **fake** are five different kinds of *test double*, and conflating them is the root cause of most complaints that "mocking makes tests brittle." The deeper disagreement behind that vocabulary is a genuine, decades-old methodological split: the **London school** (mockist) of TDD isolates the unit under test from every collaborator using mocks and drives design outside-in from the API boundary; the **Chicago school** (classicist, sometimes "Detroit school") prefers real collaborators wherever practical and reserves test doubles for genuinely awkward dependencies, driving design inside-out from the domain model. Neither is strictly correct - they encode different values (interaction verification vs. state verification), and most experienced teams use both pragmatically depending on the collaborator being tested. **When to use this guidance**: Any time you are deciding what to replace with a test double, which kind of double to use, or diagnosing why a test suite has become brittle under refactoring. ## Core Concepts Fowler's taxonomy, in increasing order of behavioral sophistication: - **Dummy**: An object passed around only to satisfy a parameter list; never actually used by the code path under test. Typically `None`, an empty object, or a placeholder with no configured behavior. - **Stub**: An object providing canned answers to calls made during the test, with no logic beyond returning what it was told to return. Used for **state verification** - the test checks the state/return value of the unit under test, not what it called. - **Fake**: An object with a real, working implementation, but one unsuitable for production - typically taking a shortcut such as storing data in memory instead of a real database. A `FakeOrderRepository` backed by a `dict` is the canonical example. - **Spy**: A stub that additionally records how it was called (arguments, call count, order), so the test can later assert on that recorded interaction after exercising the state as normal. - **Mock**: An object pre-programmed with *expectations* - a specification of exactly which calls it should receive, in what order, with what arguments - verified as part of the test, often failing immediately if an unexpected call occurs. Mocks drive **behavior verification**: the test asserts the unit under test called its collaborators correctly, not merely that it produced the correct output. | Test Double | Has real logic? | Verification style | Typical use | |---|---|---|---| | Dummy | No | None (never invoked) | Filling required parameters | | Stub | No (canned answers) | State (check the result) | Supplying test-controlled input data | | Fake | Yes (simplified) | State (check the result) | Replacing a database/service with an in-memory equivalent | | Spy | No (records calls) | Behavior, after the fact | Verifying a notification/side-effect happened | | Mock | No (expectations) | Behavior, verified inline | Verifying a specific interaction/protocol was followed | - **London School (Mockist / Outside-In)**: Start from the outermost behavior (an API endpoint, a use case) and mock every collaborator that unit calls, verifying the unit under test orchestrates its collaborators correctly. Design emerges from the interfaces needed to satisfy the mocks. Strongly associated with early Extreme Programming practitioners in London (hence the name) and the `jMock`/`mockito`-style interaction-based testing tradition. - **Chicago School (Classicist / Inside-Out)**: Build and test the domain model first with real objects, using test doubles only for genuinely slow, non-deterministic, or external collaborators (network, clock, randomness, third-party APIs). Design emerges from the domain concepts themselves, verified through their actual observable state. Also called the "Detroit school," referencing the origin of Extreme Programming's C3 project. - **The core trade-off**: Mockist tests verify *how* a unit does its job (which collaborators it calls, in what order) and therefore tend to break on any refactor that changes that internal collaboration, even when behavior is unchanged. Classicist tests verify *what* a unit produces and are more refactor-resistant, but can be slower or harder to write in isolation when a real collaborator is expensive to construct. ## Best Practices ### 1. Choose the Weakest Test Double That Achieves the Test's Purpose **Rationale**: Each step up the sophistication ladder (dummy → stub → fake → spy → mock) adds test complexity and a new way for the test to become coupled to implementation detail. Use a dummy where a value is simply required, a stub where you need to control returned data, and reserve mocks for the rare case where the *interaction itself* is the behavior under test. ```python # Dummy: the logger is required by the constructor but irrelevant to this test service = OrderService(repo=fake_repo, logger=DummyLogger()) # Stub: control what the collaborator returns, don't care how it's called class StubExchangeRates: def rate_for(self, currency: str) -> Decimal: return Decimal("1.10") # Mock: the interaction itself is the behavior under test def test_places_order_notifies_customer(mocker): notifier = mocker.Mock(spec=NotificationPort) service = OrderService(repo=fake_repo, notifier=notifier) service.place_order(command) notifier.notify_order_placed.assert_called_once_with(order_id=command.order_id) ``` ### 2. Prefer Hand-Written Fakes Over Mocking Frameworks for Persistence Ports **Rationale**: A repository's contract (`add`, `find_by_id`) is small and stable; a `dict`-backed fake is easy to write once, is reusable across every test that needs an `OrderRepository`, and - unlike a mock - actually behaves like a repository (you can add an order and then find it), catching bugs a mock configured with canned returns would miss. ```python class InMemoryOrderRepository(OrderRepository): def __init__(self) -> None: self._orders: dict[str, Order] = {} async def add(self, order: Order) -> None: self._orders[order.id] = order async def find_by_id(self, order_id: str) -> Order | None: return self._orders.get(order_id) ``` This is the same fake introduced in [Hexagonal Architecture](../architecture/hexagonal.md#6-provide-a-test-double-adapter-alongside-every-real-driven-adapter); reuse it across unit tests instead of re-deriving mock configuration per test. ### 3. Use `spec`/`autospec` So Mocks Fail When the Real Interface Changes **Rationale**: An unconstrained `Mock()` will happily accept calls to methods that do not exist on the real collaborator, silently masking a refactor that broke the contract. `spec=` (or `create_autospec`) makes the mock raise `AttributeError` for anything not on the real class/protocol. ```python from unittest.mock import create_autospec def test_charges_customer(mocker): gateway = create_autospec(PaymentGateway, instance=True) gateway.charge.return_value = PaymentResult(success=True) result = checkout(cart, gateway) assert result.success gateway.charge.assert_called_once() ``` ### 4. Reserve Mocks (Behavior Verification) for Genuine Side-Effect Boundaries **Rationale**: The only reliable way to verify "an email was sent" or "an event was published" is to check that the call happened, since there is no state change to observe otherwise. This is the legitimate, narrow use case for mocks/spies - not a general-purpose substitute for constructing real objects. ```python def test_order_placement_publishes_order_placed_event(mocker): publisher = mocker.Mock(spec=EventPublisher) use_case = PlaceOrderUseCase(repo=fake_repo, publisher=publisher) await use_case.execute(command) publisher.publish.assert_called_once() event = publisher.publish.call_args.args[0] assert isinstance(event, OrderPlaced) ``` ### 5. Run the Same Contract Tests Against Both a Fake and Its Real Adapter **Rationale**: A hand-written fake is only trustworthy if it actually matches the real adapter's behavior. Write one shared behavioral test suite and parametrize it over both implementations so they cannot silently drift apart. ```python @pytest.fixture(params=["fake", "postgres"]) def order_repository(request, postgres_session): if request.param == "fake": return InMemoryOrderRepository() return SqlAlchemyOrderRepository(postgres_session) async def test_saved_order_is_retrievable(order_repository): order = Order.create(items=[LineItem(price=Decimal("10"))]) await order_repository.add(order) assert (await order_repository.find_by_id(order.id)).total == Decimal("10") ``` ### 6. Default to the Chicago School for Domain Logic; Use London School at Use-Case Boundaries **Rationale**: Domain/entity logic (calculations, invariants) is almost always better tested classicist-style with real objects - there is rarely any collaborator worth mocking. Use-case/orchestration logic, which by design coordinates several ports, benefits from London-style mocking of those ports specifically to verify the orchestration sequence. ## Common Pitfalls & Anti-Patterns ### Pitfall: Over-Mocking Every Collaborator "Just in Case" **Problem**: A test that mocks every single dependency, including cheap, deterministic, pure value objects, ends up asserting that the mocks were configured and called correctly - not that the production code actually works. The test can pass while the real integration is completely broken, since nothing in the test touches real behavior anywhere. **Recommended approach**: Only replace collaborators that are slow, non-deterministic, external, or have real side effects. Construct everything else - value objects, simple calculators, pure domain logic - for real. ### Pitfall: Mocking Types You Do Not Own **Problem**: Mocking a third-party library's internal client (e.g., a raw `boto3` or SQLAlchemy session mock) couples the test tightly to that library's current internal call shape; a minor library upgrade can silently invalidate the mock's assumptions without the test failing to reflect the real new behavior. **Recommended approach**: Wrap third-party dependencies behind your own port/interface (see [Hexagonal Architecture](../architecture/hexagonal.md)) and mock or fake *your* interface, never the vendor SDK's internals directly. Verify the wrapper itself with a real integration test. ### Pitfall: Asserting on Mock Call Order/Arguments That Are Implementation Detail, Not Contract **Problem**: A test that asserts a collaborator was called with an exact positional argument list, or in one specific order among several calls that could legitimately happen in any order, breaks on harmless refactors and produces "test is lying about what's broken" fatigue. **Recommended approach**: Assert only on the interactions that are actually part of the contract (e.g., "an email was sent to this address," not "logger.debug was called exactly 3 times before repo.save"). ### Pitfall: Confusing a Fake's Simplifications With the Real System's Behavior **Problem**: An in-memory fake repository has no unique-constraint enforcement, no transaction isolation, and no query performance characteristics - tests that pass only against the fake can hide bugs that only manifest against the real database (e.g., a race condition, a constraint violation). **Recommended approach**: Treat fakes as a unit-testing accelerant, never a substitute for integration tests against the real dependency - see [Integration Testing](integration_testing.md). ### Pitfall: Rebuilding Mock Setup From Scratch in Every Test **Problem**: Copy-pasted, slightly-different mock configuration across dozens of tests makes a later contract change require editing every test file. **Recommended approach**: Centralize mock/fake construction in fixtures or factory functions, one place to update when a collaborator's interface changes. ## Testing Strategies - **Match the test double to the pyramid layer**: unit tests (Chicago-style, real domain objects, fakes for infrastructure) form the base; use-case-orchestration tests (London-style, mocked ports) sit just above; integration tests replace fakes with real adapters - see [Unit Testing Principles](unit_testing_principles.md#the-test-pyramid). - **Verify fakes against real adapters periodically**: a shared parametrized contract-test suite (Best Practice #5 above) should run in CI on every change to either the fake or the real adapter. - **Use `pytest-mock`'s `mocker` fixture** over raw `unittest.mock.patch` decorators for readability and automatic cleanup between tests. - **Prefer dependency injection over `patch()` on import paths**: patching `module.ClassName` by string path is brittle to refactors (moving a class breaks the patch path silently); injecting the collaborator (constructor or FastAPI `Depends`) makes substitution explicit and type-checked. **Recommended test stack (Python)**: `unittest.mock` / `pytest-mock` for stubs, spies, and mocks with `autospec`; hand-written classes for fakes; `pytest.fixture(params=...)` to share contract tests between fakes and real adapters. ## Performance, Security & Scalability Considerations - **Test doubles keep the fast unit layer fast**: replacing slow collaborators (network calls, database round-trips) with stubs/fakes is what allows the base of the test pyramid to run in milliseconds - this is a direct performance property of the test suite itself, not of production code. - **Security-sensitive collaborators still need a real-adapter check**: a mocked authentication/authorization port can make a test suite green while a real integration bug (e.g., a misconfigured token validator) reaches production; always pair mocked auth-port unit tests with at least one integration test against the real auth mechanism. - **Mock/fake drift is a maintenance cost that scales with team size**: the larger the team, the more valuable Best Practice #5 (shared contract tests between fake and real adapter) becomes, since independently-maintained fakes drift apart faster with more contributors. ## Edge Cases & Advanced Usage - **Testing retry/backoff logic**: A spy on a flaky stub (one configured to fail N times then succeed) is the right tool to verify retry counts and backoff behavior without needing a real unreliable network. - **Testing idempotency**: Use a spy to assert a side-effecting collaborator (e.g., a payment charge) is called at most once even when the calling code is invoked twice with the same idempotency key. - **Partial mocking (mocking one method on an otherwise real object)**: Occasionally useful for legacy code with an expensive-to-construct dependency, but treat it as a last resort - it signals the class likely has too many responsibilities and would benefit from extracting the expensive part behind its own port. - **Time-based testing**: Prefer a fake/stub `Clock` port over patching `datetime.now` globally; a global patch affects unrelated code running in the same test process and is a common source of hard-to-diagnose cross-test interference. - **Testing concurrent/async code with mocks**: Ensure mock methods intended to be awaited return coroutines (`AsyncMock` in Python's `unittest.mock`) - a plain `Mock` used where `await` is expected fails in a confusing way. ## When *Not* to Use / Alternatives | Dimension | Mocks (behavior verification) | Fakes/Stubs (state verification) | Real Collaborator (no double) | |---------------------------------------|--------------------------------------|-------------------------------------|----------------------------------| | Refactor resistance | Low (couples to internal calls) | High | Highest | | Best for | Verifying a side effect occurred (notification, event publish) | Supplying/controlling data, replacing infra | Cheap, deterministic, pure collaborators | | Speed | Fast | Fast | Depends (fine if genuinely cheap) | | Risk of false confidence | High if overused | Moderate (fake must match real adapter) | Low | | School most associated with | London / mockist | Chicago / classicist | Chicago / classicist | **Avoid mocks entirely** when a real object is cheap to construct and deterministic - most value objects, calculators, and small domain entities need no test double at all. **Prefer the Chicago/classicist default** for a codebase's domain layer; reserve London/mockist-style interaction testing for use-case orchestration and genuine side-effect boundaries (notifications, event publication, payment charges). ## Related Documentation - [Unit Testing Principles](unit_testing_principles.md) - where test doubles fit within a healthy test pyramid. - [Integration Testing](integration_testing.md) - verifying the real adapters that fakes and stubs stand in for. - [Hexagonal Architecture](../architecture/hexagonal.md) - ports are what make clean fakes/mocks possible in the first place. - [Clean Architecture](../architecture/clean_architecture.md) - use-case boundaries as the natural seam for London-style mocking. - [SOLID Principles](../principles/solid.md) - Dependency Inversion and Interface Segregation underpin substitutable test doubles. - [FastAPI Best Practices](../../languages/python/frameworks/fastapi/best_practices.md#testing-strategies) - dependency overrides as FastAPI's built-in substitution mechanism. ## Glossary - **Test Double**: Generic term (coined by Gerard Meszaros) for any object substituted for a real collaborator during a test. - **Dummy**: A test double passed only to satisfy a parameter list; never actually invoked. - **Stub**: A test double providing canned answers with no logic; used for state verification. - **Fake**: A test double with a real but simplified/unsuitable-for-production implementation (e.g., in-memory repository). - **Spy**: A stub that also records how it was called, for later assertion. - **Mock**: A test double pre-programmed with expectations about the calls it should receive, used for behavior verification. - **State Verification**: Asserting on the observable output/state produced by the unit under test. - **Behavior Verification**: Asserting on the specific interactions/calls the unit under test made to its collaborators. - **London School (Mockist)**: A TDD style that isolates the unit under test with mocks for every collaborator, driving design outside-in. - **Chicago School (Classicist/Detroit)**: A TDD style that prefers real collaborators, using doubles only for genuinely awkward dependencies, driving design inside-out.