Have you ever doubted that Unit Tests are worth the effort? Or have you had a feeling that you can’t rely on them in terms of quality? It might be because you’ve been exposed to so-called “London School” of writing tests.

This text is my attempt to summarize and draw conclusions from the pragmatic approach to Unit Tests and TDD presented in the best resources I could have found.

Goals vs reality

Let’s start with some promises Unit Tests and TDD made:

  • Finding bugs during development and when adding new features
  • Safe refactoring
  • Stable and fast feedback
  • Faster software delivery
  • Confidence in code

Now let’s look at some issues people face with Unit Tests:

  • Code is hard to refactor - A slight change to any method call stops tests from compiling. After refactoring the tests’ code changes as well, thus all mocks must be fixed (or added/removed). In the end, it doesn’t look like a safety net that was promised by Beck Kent in “Test Driven Development: By Example” or M. Feathers in “Working Effectively with Legacy Code”.
  • Tests are fragile - Adding any non-trivial feature causes failed compilation of tests. It’s not because the behavior is changed, but because the internal structure was adjusted to contain a new feature.
  • Bugs are seldom found - Due to problems described above, tests are often written after the code and in a way that mirrors the class under test.
  • Testers or Integration Tests find obvious bugs - Even though classes were tested in isolation with 100% of coverage, they don’t play together during a real interaction.

Characteristics of proper Unit Tests

The aim of this text is to show that the issues listed above are not caused by the tests themselves, but rather by the way we write these tests. In particular:

  • Mocking interactions between classes we own,
  • Treating a class as a “unit”.

It turns out these problems were introduced by the so-called “London School of TDD”. For a long time I wasn’t even aware there’s an alternative, or rather, that there’s an original approach called “Chicago School of TDD” (aka Classical or Detroit School). By the way, the original approach was the one that pushed TDD into the mainstream.

In Chicago style TDD/Unit Tests are:

  • Aiming to test the behavior of the system - In the result you test real requirements instead of declared internally plumbing. It means that the final state is more important than internal interactions between classes. Later a term BDD was born to emphasize it’s not the “unit” that’s important (a remark: Gherkin and Cucumber didn’t exist yet. It was BDD in programming language).
  • Treating modules (not classes) as units of tests - When you have a set of classes that work together, they should be tested together as well. It makes sense, because interactions are sometimes more important than the logic of a class. It also simplifies and speeds up writing tests.
  • Using mocks only on module boundaries - Mocking internals results in fragile tests and makes them harder to write/maintain. It doesn’t mean you shouldn’t use mocks at all. Yet it’s worth limiting their usage to module boundaries and places where “it would be awkward” otherwise (e.g. relation is unstable or “far away”).
  • Making refactoring easy - When we combine the bullet points listed above, we will get rid of problems listed at the beginning of this article. Tests will be a real safety net when we refactor the module, they won’t require that much maintenance and give you confidence about the quality you deliver.

Let’s see how it looks in practice:

  • Given a handler class and a command validator class, with a holistic approach of Chicago School, the validator should not be tested separately. Instead, the external behavior of the handler is what we need to test. And that includes a validation of its parameters with whatever approach it uses (tests won’t break if you change the way you validate the parameters). (BTW, I’ve used this example because I’ve seen a bug once which was caused by a validator that - although tested - was not injected into DI thus not invoked).
  • When mocking an interaction between class A and B, we setup mock’s behavior to mimic an expected behavior from class B. But what if the behavior of class B changes with time (but interface stays the same) and nobody updates the mock’s responses? Tests of class A are not trustworthy anymore.
  • The “Red - Green - Refactor” approach of TDD is not possible with the London School. Getting the code to the “Green” stage means that the tests are ready and can be frozen. We should be able to do optimizations, class splits, or other refactorings, and tests should not break. In theory we could start with a “big ball of mud”, get the tests passing and only later - having the test as a safety-net - fix the design without changing module’s external behavior.
  • Don’t test declarative code directly. E.g.: when you use a library like FluentValidation (that lets you write a validation logic in a declarative manner), unit testing such a class is a waste, because you test that you declared what you wanted to declare.
  • Tests are green but the app doesn’t work? It’s very common with the London (mocks) style of testing.

Closing thoughts

Closing remarks for the ones that got intrigued (and for the sake of my compilation’s completeness):

  • Orthogonally to Chicago and London Schools there are two possible approaches: “Inside-Out” and “Outside-In”. The “Inside-Out” style means that we start at the core of the code and move up to the outside (program entry point or UI). This style is more often seen with Chicago School, because without mocks you can’t start coding on the boundaries. But if you do “Outside-In” approach with so called “Overlapping Sociable Tests” approach.
  • Martin Fowler uses the term “Sociable” to describe tests without mocks and a term “Solitary“ for mockists’ approach. He says that he preferred the Sociable tests and was once accused of using the term “unit test” wrong (link below).
  • Uncle Bob (Robert C. Martin) is also a known fan of Chicago School of TDD. In his text (link below) he describes problems they encountered when starting their project with a London approach and why they switched to a Chicago style.
  • My text is not about the superiority of Test Driven Development over “tests after” approach. Although while preparing the materials I’ve got inclined even more to the TDD camp.
  • Mocks are still useful when you need to use “Low gear” mode. It’s a TDD practice that is used when you tackle a problem that is hard to describe in terms of system behavior. E.g. a class with complex algorithms. In such a case it’s worth “changing down gears” (car metaphor) and isolating the class with mocks. Just don’t stay in the low gear for too long.
  • Chicago School should not be confused with Integration Tests. Although it uses the behavioral style and sometimes a DSL layer, we don’t test anything that we haven’t written. Integration Tests/Component tests are still required to e.g. check how requests are parsed, whether external systems are called or if data are written correctly to the DB.
  • I didn’t cover the topic of “Test Pyramid and test duplication”. But I wanted to mention that the creator of the Ruby on Rails wrote once that “TDD is dead”. But my conclusion from the debate that took place later is that you can’t blindly apply a standard Test Pyramid to every system.

To wrap up, whenever I’ve convinced you or not that Chicago School has more advantages than the London School, I encourage you to learn more about that subject from the resources I’ve listed below. They will show you that I’m not reinventing a wheel and give you a broader perspective and insights on best practices for TDD and Unit Tests.

Resources

Key resources

Other useful resources


Andrzej Nowik/2023.01.16/v1.1
.