It's an Integration, Not a Choice
Now that you’ve mastered the basics of Test-Driven Development, consider the two primary schools of TDD. The London School takes an outside-in, behavior-based approach, which fosters Command-Query Separation, and relies heavily on test doubles — ceding (somewhat) brittle tests. The Chicago School takes an inside-out, state-based approach, which promotes high cohesion, with a greater emphasis on design patterns — although YAGNI is a risk.
But it's not about choosing one over the other. It's about understanding your quality model and driving optimization towards those qualities that need to be optimized. London and Chicago each have their pros and cons. The best approach to TDD is an integrated adoption of these two schools.
The London School provides a formal, behavior-based approach to TDD by starting from the outside of the application (typically from the APIs or controllers) and works in towards the lower layers, including domain models and down to the persistence layer.
This approach helps to flush out how users will navigate the application by starting at the entry points of your system and working your way down to lower layers. Obviously, this will require heavy use of test doubles as you drill your way down. While this helps to keep your code base small and ensures you're not writing dead code, it does tend to create brittle tests — making refactoring more difficult, which discourages continuous refactoring.
Focusing on behavior helps to manage side effects by promoting pure functions — providing a gateway to functional programming.
While test-driving out behavior helps to keep your code base small and ensures you're not writing dead code, it does tend to create tests that break easily.
Having tests that are tightly coupled with your production code will make continuous refactoring very difficult and time-consuming. And this is one of the biggest drawbacks of top-down TDD — dealing with broken tests with most code changes.
The Chicago School (a.k.a. Detroit School) provides more of an informal, exploratory, state-based approach to TDD by starting from the inside of the application (typically from the domain models) and works out towards the APIs.
Since we are starting at the lower levels of the architecture, we are continuously building on prior tests. This tends to produce tests that are decoupled from the implementation — enabling aggressive refactoring of the implementation without breaking the existing tests. This in turn provides a highly redundant regression test suite that provides a strong safety net for continuous refactoring.
As we progressively write very specific tests to more generalized tests, the resulting production code becomes highly cohesive. As the tests become more general, the production code becomes more specific. This promotes high cohesion. And with high cohesion comes loose coupling. Which in turn promotes high code quality including maintainability, testability, extensibility, etc.
Building from the inside out requires far fewer test doubles since we are building on top of previously written tests. There is rarely a need to stub out or mock dependencies since, in most cases, we build them as a result of prior (lower) tests. This helps to develop a more reliable, less fragile test suite.
Building from the inside often fosters over-engineering — writing code that is often not needed at the end of the day.