Setting the record straight about TDD | Part I: Introduction

A lot has been written on Test-Driven Development. Yet, as software developers, we can’t seem to agree on a clear roadmap to TDD adoption: while any experienced developer surely knows the basics, there remains widespread confusion about practical do’s and don’ts. In this series of three articles, I will get to the bottom of TDD best practices by combining theory (knowledge acquired through my readings) to real-life use cases (straight-up from a developer who fast-tracked his way up the corporate ladder).

But first things first: let us start by laying the foundation for Test-Driven Development and exploring core concepts such as the red-green-refactor (RGR).
Let’s briefly cover the steps associated with the RGR process. Say I were to write a function that adds two numbers:

1st step is writing the test (even if the code doesn’t compile).
The step is called Red because the test fails and is red, since no implementation has been written yet = Red phase:

public void adding_2_3_returns_5(){
  assertThat(new MyCalculator().add(3,5)).isEqualTo(5);
}

2nd step is fixing the test as quickly as possible for it to pass.
For this step to succeed, we need to implement the “add method” in the MyCalculator class = Green phase:

public class MyCalculator {
  public int add(int a, int b){
    return a+b;
  }
}

Now that the “add method” is implemented, the test passes. Oh happy day! For this 101 example, we will skip the 3rd step since no refactoring is needed. However, refactoring should always happen at this stage.
We will go back to refactoring later as it is a critical step in the RGR process.

For the time being, let us take a moment to think about what we have achieved so far: we have created our first piece of code; we have created a Unit Test with an expected return value; we have made sure that the function implemented returned this value.

Given that most ambiguities and misconceptions about TDD today arise from the basic definition of a Unit Test, let us start by agreeing on what is a Unit Test. In my POV, a Unit Test should, above all else:

  1. Verify a piece of code (unit)
  2. Do it quickly
  3. Do it in an isolated manner

Straightforward. Perfect. You’re sold. Moving on… right? Actually, whereas experts agree on the “simple”, theoretical definition, considerable disagreement exists on what is meant by “isolated”. Two main schools of thought have thus emerged: the Classical school (or Detroit school) which defines isolation as unit tests running in isolation from each other – i.e. they don’t share any common state – and the London school which defines isolation as the isolation of the system under test (SUT) from its collaborators / dependencies.
In other words:
– in the Classical school = you only mock dependencies that provide a shared state
– in the London school = you mock all the SUT dependencies

What the hell does that mean? Nothing speaks better than an example:

public class BookStore {
  private final ProductInventory;
  
  public BookStore(ProductInventory productInventory){
    this.productInventory=productInventory;
  }
  public boolean purchaseBook(String bookTitle){
    if(productInventory.isPresent(bookTitle)){
       productInventory.remove(bookTitle);
       return true;
    }else{
       return false;
     }
  }
}

Let’s first do it the Classical school way (Classical school implementation):

public class BookStorePurchasingTest{
  @Test
  public void should_return_true_when_book_is_present(){
    final String bookName="Le Comte de Monte Cristo";
    final ProductInventory pi = new ProductInventory();
    pi.add(bookName,1);
    final BookStore sut = new BookStore(pi);
 
    final boolean isAvailable= sut.purchaseBook(bookName);
  
    assertThat(isAvailable).isEqualToTrue();  
  }
  
  @Test
  public void should_return_false_when_book_is_absent(){
    final ProductInventory pi = new ProductInventory();
    final BookStore sut = new BookStore(pi);
 
    final boolean isAvailable= sut.purchaseBook(bookName);
  
    assertThat(isAvailable).isEqualToFalse();  
  }
}

Now let’s do it the London school way (London school implementation):

public class BookStoreTest{
  @Test
  public void should_call_pi_remove_when_book_is_present(){
    final String bookName="Le Comte de Monte Cristo";
    final ProductInventory pi = Mockito.mock(ProductInventory.class);
    final BookStore sut = new BookStore(pi);
    when(pi.isPresent(bookName).thenReturn(true);
    sut.purchaseBook(bookName);
    verify(pi,times(1)).remove(bookName);
      
  }
  
  @Test
  public void should_not_call_pi_remove_when_book_is_absent(){
    final String bookName="Le Comte de Monte Cristo";
    final ProductInventory pi = Mockito.mock(ProductInventory.class);
    final BookStore sut = new BookStore(pi);
    when(pi.isPresent(bookName).thenReturn(false);
    sut.purchaseBook(bookName);
    verify(pi,times(0)).remove(bookName); 
  }
}

Before we dive any deeper, let’s take a step back and understand the implications of this code. The use case is simple; we are testing whether upon purchasing a book, the code can check the book availability in the inventory.
If the book is available, it is removed from the product inventory. The code returns true.
If it is not available, the code returns false.

We have a BookStore class with a dependency on ProductInventory.
The Classical school way of doing it: we inject a real dependency with real values and test the observable behavior (the value of the return function).
The London school way of doing it: we create a mock for this dependency and test the interaction with this mock (we verify that the methods in the ProductInventory class are called with the correct parameters).

Furthermore, do notice that the name of the test class differs: in the classical school, every behavior has its own test class; in the London school, every class has its own test class.

Let’s summarize the differences between both schools in a small table:

Classical SchoolLondon School
Works with stateWorks with interactions
Mocks only shared dependenciesMocks all private / shared dependencies (but not immutable)
Test runs in isolation from other testsTest written in isolation of other dependencies
A unit = set of classes = a behaviorA unit = a class

On paper, both approaches work and both schools argue that the written code behaves as expected.
Let’s recap once more time before moving ahead: in the Classical school, we are injecting a real dependency and testing against a behavior instead of interactions. In the London school, we are mocking all interactions with dependencies and testing against those interactions.

So which approach should we adopt? It really comes down to two considerations: what are we testing and what are we mocking.
In the Classical school, we are testing a behavior; in the London school, we are testing a class’s interaction. That’s for the testing part. As for the mocking part, here’s a diagram to better understand what’s at stake and hopefully make sense of what each approach implies:

All dependencies fall under one of three categories: public shared, private mutable or private value object:

  • Shared: a database, a static mutable field, a filesystem…
  • Mutable: dependency that is not shared and not a value object (e.g. ProductInventory)
  • Value Object: a static final variable : final static String name=”Name”;

The Classical school advocates the mocking of the shared dependencies only (whether out of process or not).
The London school advocates the mocking of both the shared and the mutable dependencies.
And, fundamentally, that is the sticking point.

It is getting late though… While you ponder over what we have covered thus far, I will get a head start on the following article which will explore the ins and outs of unit testing: starting with what is a good unit test, I will go on to reveal which school of thought I personally follow and what are my reasons for doing so. The answer will not be all black or white, meaning that there is no easy or simplistic magic formula. But then again, is there ever?

Until we meet again, I’d be interested to know your reflections / key takeaways on TDD based on your experience: what school of thought do you follow and why? Moreover, I will be happy to answer any questions you have. So drop a line in the comments below with your questions, feedback, thoughts, or just to say hi! 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *