TDD Process

Heaton Cai
7 min readAug 15, 2021

--

TDD has been misunderstood by many people for many years. Some of them think TDD is just about writing code. It is just a dev skill that starts with writing a random test and assume the rest of the test cases would come out by themselves. Others think TDD starts with a pre-design that some entities and public methods need to be worked out first so that they can write tests (possibly with stubs/mocks) against the designed public methods (APIs). They didn’t realise these public methods could be the implementation detail and be private.

In this article, I’m going to describe how to do it right. Let’s start with the definition, then all the steps we need to go through during the TDD process.

What is TDD?

TDD is a process that converts requirements into test cases to drive out the functional code with the best design.

Based on the definition, TDD should start with understanding the requirements (the preparation phase) before coding and end with all requirements developed.

TDD Process End to End

The preparation phase is usually confusing people. It is not about design, not about a solution, it is only about requirements. Let’s walk through the process with an example.

The kata:

You work for a company that provides taxi services with self-driving cars. The pricing team decides to charge each ride based on how far it travels and how long it waits. The minimal charge is $6 which covers the first 2 km. Then add $1.2 per km after 2 km. Additionally, add an extra 20% per km after 8 km. The waiting time is charged by $1.5 per minute with free waiting for the first minute. The total price is rounded to the nearest whole number.
You are asked to write a program to calculate the price.

The TDD Process

Let’s start with the requirements.

Phase 1: Preparation Before Coding

Goal: understand the problem without thinking about the implementations

In many demos from the TDD masters, you might see them writing the first test straight away without the preparation phase. That is because they have done this so many times, thus they are able to finish it in the brain in an instant. I highly recommend you to do this on a whiteboard or a paper if you are a TDD beginner.

The preparation phase has three steps.

Step 1: Tasking

List all the scenarios. This helps us to have an overview of the problem.

Tips:

  • Scenarios should relate to the problem, not the steps to solve the problem. For example, some people might think about how to add different types of charges, which is the implementation, not the scenarios.
  • Solving all scenarios should solve the problem.
  • Try to stay high level so that you are not overloaded by the detail.

The example:

Step 2: Design the interface

Work out how the interface looks like. Changing the interface during the implementation might require changes to all the existing tests. So it is better to make it proper at the beginning. And it can be easily done by finding the input and output of the function.

Tips:

  • The interface should focus on the problem instead of the steps in the solution. For example, calculateBasePrice is a wrong name for the interface because it is just a step in the implementation.
  • Find out the minimum required inputs and outputs to solve the problem.
  • Use the format Something.do… to describe the interface is helpful to write tests.

The example:

  • Input: distance: Double, time: Double
  • Output: price: Int
  • Interface: Taxi.calculatePrice(distance: Double, time: Double): Int

Step 3: Design Test Cases

Now we can list some test cases for some scenarios based on the inputs and outputs designed from the previous step.

Tips:

  • Test cases should be specific.
  • From simple to complex and keep small differences between test cases
  • Just enough to start, covering all scenarios is not required since they might be changed later.

The example:

Just like I mentioned, we don’t need to plan all the test cases. We also don’t need to stick to the plan during the implementation. The plan can be changed in some cases like:

  • add a test if you find it needs to be added to drive some code or refactoring
  • remove the test if you find it passed without changing any functional code (it means it has been covered by other test cases already)
  • change the order if you find it is easier to drive the implementation

Now the preparation phase is finished. Let’s start to do some coding.

Phase 2: Coding

Goal: write tests that drive the implementation for all the requirements

This is a well-known phase that includes 3 steps in a cycle. Once the preparation phase is done well, we should be able to follow the steps easily, except for the first test.

The First Test

Many people are not able to write their first test to drive the first piece of implementation. However, it is a very important skill to avoid writing tests against implementation. If the implementation was written first, it would end up with writing tests against this implementation that might not be the proper interface.

So remember that in TDD, every line of functional code should be driven by tests, including the class and method definitions. No exceptions. It means the first test should not compile. This gives us the flexibility to adjust the testing code to be more readable and to make the interface to be more consumer-friendly. It also helps us to focus on the requirement instead of the implementation. Here is an example.

The test should meet the following criteria

  • It should describe some business value
  • It should be readable by a non-technical person
  • It should not compile
  • It should be able to be easily implemented without changing the test
  • It should pave the way for the coming tests

After the first test is done, we could leverage the IDE’s capability to easily create the classes and methods. It is more efficient than creating them manually.

create a class from the test

Now, we can go through the steps of the TDD cycle.

Step 1: wear a QA hat to write a test for the next bit of functionality you want to add

To make sure every line of code has value, it requires every test describes values and all code is driven by each test. Wearing a QA hat in this step is very important that helps to ensure tests are against the business value instead of implementations.

Tips:

  • Start with the planned test case.
  • Only write one test case at one time.
  • Make the test name describe the scenario. Such as Taxi charge calculator should return $6 as a base charge for a ride < 2 km.

Step 2: wear a Poor Dev hat to write functional code that is just enough to pass the test

In this step, we need to through all the knowledge of good practices away (which is hard), forget about clean code, forget about the design, and only focus on passing the test with a minimum code change. Overthinking and over-design slow us down in the process.

Tips:

  • Do not think about the design, focus on just passing the test. You can hard code the result or just use an if-else statement. Many people failed to do so and end up mixing step 2 and step 3 together, which is risky to introduce unnecessary code that isn’t covered by tests.
  • Try to not change the existing code, only add extra code for the new test case.
  • Keep the change minimum and simple. Try to avoid introducing more than one additional line of code.

Step 3: wear a Good Dev hat to refactor both testing and functional code to make it cleaner

Now it is time to bring the knowledge of clean code, design principles back. In this step, the most important thing is to make sure no extra functions are introduced.

Tips:

Repeat the above steps until all requirements are fulfilled

Tips:

  • Follow Baby steps to make it easy to follow the process.
  • Commit at least once for each iteration. Committing frequently helps you to try and compare different ways of implementation.
  • Revert to the previous version if you are stuck in one step for a long time.
TDD steps during coding

Once all the above steps are finished, the TDD process is finished.

Summary

Many developers failed to do TDD is because they start in the middle or try to write tests against the implementation. The TDD process as its name actually asks developers to drive all the functional code by requirements. It asks developers to focus on the requirements instead of the implementation. It asks developers to think less during the coding phase so that there are fewer human mistakes. By following the TDD process, we can reduce the subjectivity to the minimum (so that less over design) and keep the cleanest code all the time.

Reference

--

--

Heaton Cai

16 years in the IT industry, passionate to share what I have learnt. All thoughts and opinions are original and maybe new. Free to share with the original link.