Note: "Mocks" in the context of this post refers specifically to one kind of "Test Double" used as a first class citizen for the testing paradigm, not the popular use of "Mock" to represent ANY kind "Test Double".
There are two schools of TDD (Test-Driven Development). The Detroit school (classicist, state-based, black-box) says: test with real dependencies, which are dependencies also used in production code. Pass inputs, check outputs, all within the Domain layer without communication with the external world. The London school (mockist, behavior-based, outside-in) says: isolate what you want to test (the "unit"). Mock all of its dependencies. Test the contract between them only.
Martin Fowler named the split “classicist vs mockist” in Mocks Aren’t Stubs, first published in 2004. The city (Detroid/London) labels came later. Michael Feathers and Steve Freeman proposed “Detroit” and “London” at QCon London in 2009, naming each school after where its practices took root.
Detroit for the Chrysler C3 project near Auburn Hills, where
codified TDD in the mid-1990s. London for the Extreme Tuesday Club and the Connextra developers who invented mock objects and published the technique in Endo-Testing: Unit Testing with Mock Objects at XP 2000.
Most teams pick one school and treat the other as wrong. That framing misses the point. Each school fits a different design paradigm.
So it's time to leave the ego aside and analyse them from an unbiased perspective:
Detroit
Detroit-style testing works like functional programming. A pure function takes a value and returns a value. You don’t need to mock a value. You just pass a different one.
This isn’t just an analogy. Gary Bernhardt’s Functional Core, Imperative Shell argues for isolating pure logic from side effects and testing the pure core with state-based tests, no mocks needed. Mark Seemann’s From Dependency Injection to Dependency Rejection takes the same position.
A functional design eliminates the need for test doubles in unit testing, you use the real functions, the real classes, the real models.
London
Mockist testing works like classical OOP. The unit talks to dependencies through interfaces. The interesting question is not “what came out” but “how does this unit behave across the full range of behaviors the interface permits?”
Steve Freeman and Nat Pryce codified this in Growing Object-Oriented Software, Guided by Tests. The book’s title carries the point: the testing style grows from the design style. Mockist TDD has a known pull in the other direction. The pain of mocking is design feedback. A unit with many collaborators is miserable to mock. That misery creates the incentive for fewer collaborators and a pure core.
For that reason, the Test Double wiki calls London-school TDD “a gateway drug to functional programming.” The pull runs through the design, not the test mechanism like Detroit-school. A mock surfaces the problem. Fixing it moves the code towards a functional shape.
The debate between the two schools is not about testing technique. It’s about design, and neither of them is more "right" or "wrong" by itself.
Mocks are meant for design, not determinism
A common defense of mocks goes like this: they make tests deterministic. Replace the database with a fake and remove the network call. Reusable Mocks can be organised in test-specific models that implement production interfaces.
Get Fayner Brack’s stories in your inbox
Join Medium for free to get updates from this writer.
After that, the test runs fast and repeatable. The side effect, not the purpose. You define the precondition through the mock. The assertion checks the postcondition and the unit’s logic is the only variable.
If you inject the real dependency, that isolation disappears. The failed test gives you three potential suspects: the unit, the dependency, or a recent change to the dependency.
Freeman and Pryce put it wisely in Mock Roles, Not Objects: the most important benefit of mock objects is what they called “interface discovery.” Mocks drive narrow role interfaces by forcing you to define exactly what the unit needs from its collaborators. The technique comes from Wirfs-Brock’s Responsibility-Driven Design. It serves the same goal: small, focused units with explicit contracts.
The strongest argument against mocks: they couple tests to implementation. Refactor the internals and the tests break. You end up spending more time fixing tests instead of investing time in improving the code. The diff generates changes on both test and production code, even if the behaviour of the production code hasn't changed.
Imagine a team stubs a method to return a certain shape. The real dependency later changes its return contract, from nil to an empty array for example. The stubbed test stays green and production is the only one that breaks. This happens often in codebases without static typing, where nothing enforces the contract between a mock and the real object.
The mock should not recreate an existing implementation. Its job is to explore the contract defined by the interface. Mock roles, not objects.
The mockist answer is disciplined practice, not denial. Mock only what you own: don’t mock third-party types. Mock actions, stub queries. Keep a thin layer of acceptance tests to catch integration gaps.
Use contract tests to verify that fakes still match reality. Without these guardrails, mocks produce the brittle tests critics describe. With them, mocks probe boundaries that state-based tests can’t reach.
Where FP still needs test doubles
Pure functions don't need mocks when the architecture pushes impurity to the edges. The pure core is easy to test with inputs and outputs. The impure shell still needs test doubles.
FP languages solve this with different machinery. Clojure uses with-redefs to stub out impure functions during tests. F# and Scala pass stubs through partial application or the Reader monad. Haskell uses free monads or tagless-final, patterns that serve the same role as an interface in OOP: runtime substitution of effects.
FP doesn’t escape what mocks solve. It relocates the problem to architecture.
The pure core doesn't need test doubles. The impure boundary uses higher-order functions instead of interfaces to achieve the same isolation.
The mockist-OOP approach is about Dependency Injection machinery for runtime substitution. FP avoids that machinery by designing differently, not by having nothing to substitute.
Where the schools meet
Both schools agree on integration tests. Unit tests show individual pieces work. Integration tests show the pieces produce the expected aggregate behavior when connected.
The difference is what “individual piece” means. In mockist TDD, a piece is a unit isolated by mocks. In Detroit TDD, a piece is a cohesive function or module tested with real collaborators.
Neither of them is wrong. Each answers a question shaped by its design paradigm. Experienced practitioners treat the schools as tools, not tribes.
Teams that write OOP and test Detroit-style feel friction. Teams that write FP and test mockist-style feel it too. The testing style wants to match the design style. When they don’t match, the tests fight the code. Be pragmatic… Tools are just tools.
Most teams go to war on which TDD school is better, like spaces versus tabs.
A more useful question is: Which paradigm does the code follows and, therefore, which one should I continue?
If you liked this, you might like readplace.com, built for exactly this kind of reading.
Thanks for reading. If you have some feedback, reach out to me on LinkedIn, Reddit or Github.