Let’s talk about tests. Tests are the documentation for the code, they explain the behaviour of the code, and they prevent changes to the code from breaking the expected behaviour. Without test coverage, bugs make their way into production, the effort for fixing bugs becomes bigger than the effort for writing new code, and new functions become annoying to implement.
Firstly, because the code is designed with testing in mind, the developer already knows what functionality the code should provide. This prevents developers from writing code which is hard to test, or too complicated, driving better code quality as a result.
Secondly, writing a test after the implementation is more like adding a test for the current implementation, by doing that the developers are likely to ignore what functionality the test should ensure; TDD can ensure that the implementation is just enough to meet the requirements, no less or no more.
TDD takes an iterative approach to writing software. A test is added as soon as a requirement is clear. This test intends to fail at this point as the implementation code is yet to be written. The next step is to write the code to make the test pass quickly. After the test passes, we check the code and refactor it to make the code better. Don’t forget to rerun the test after refactoring to ensure that it passes. Once complete, we pick up the next requirement and follow the same process. TDD is the art of changing the colour of tests from red (failed) to green (passed).
TDD always starts with a test. We follow the “Arrange-Act-Assert”(AAA) pattern to add a test. The process for writing a unit test is split up into three phases:
Tip: Keep some space between each section when writing the code to allow the reader to better understand which method is being tested. This becomes the custom in teams that are accustomed to writing unit tests instead of comments. TDD is the art of changing the colour of tests from red (failed) to green (passed).
The test will look like this:
Here we will use an example to demonstrate the process of TDD. We are going to write a Greeter program, it will return “Hello World!”, when we pass in some names (John, Lisa, Ravi) to it, it will return “Hello John, Lisa, Ravi!”. The code for demonstration is written in C#, and the test framework is XUnit, but I think it should be easy to understand if you use other programming languages like Java, Python, Kotlin, etc.
Normally the TDD cycle should be short enough so that we can get quick feedback when some requirements are satisfied. The test is the acceptance criteria of the requirement, and we should not put multiple scenarios into one test. Tasking is the first job once the requirements are received, we need to split the whole requirements into several smaller tasks, and then tackle them one by one.
We can split the above requirement into the following tasks:
Let’s begin with the first requirement.
Follow the AAA test structure, we will have the first test looks like:
If we don’t have the Greeter class yet, the compiler will complain, so we need to write just enough code to make the compilation successful.
Now if we run the test, the test should fail as expected, as we haven't written the implementation yet. Let’s make the test pass by changing the return string to “Hello World!”.
The next thing is to see if we need to refactor the code to make it more readable and maintainable. Apparently there is not much we can do for this one line code, let’s continue with the next requirement.
In this part, we will pass the Greet method a name, and want to see the greeting message accordingly. We need to add another test for this.
In order to make the compilation successful, we need to add a parameter to the Greet method. Then we need to run the two tests together. This is very important, as we need to make sure our change did not break the previous functionality. We should see the previous test passed, but the new one failed. Then we update the code to make it pass.
In this part, we need to handle multiple names. Follow the same process, we will have a new test looks like:
Also we need to update the Greet method to allow passing multiple names. Then after we run all 3 tests, we will get one failed test. Let’s make it pass quickly:
After we finished all the requirements, the product owner found that there is a grammar error when greeting multiple names. The output should be corrected as "Hello John, Lisa, and Ravi!".
This is a change for the existing requirement, so we need to update one of the existing tests to reflect the new requirement.
Without surprise, the test will fail. Then let’s update the code to make the test green again:
Now we have fully implemented a Greeter with all requirements satisfied. Please do remember to check if we can do some refactoring to make the code cleaner.
There is not much to do with the Greeter class, but when we look at the code, we can see that we have 3 very similar tests, the only difference is the input and output. Test framework like NUnit provides us a neat way to organise this kind of tests by using TestCase attributes. The updated test will look like as below:
In this blog, we talked about the basic things of TDD such as TDD cycle, test structure, tasking and how to TDD step by step. Sometimes there might be code which cannot be run within the context of a test and we might not have control over how it will be implemented. We will handle such cases in an upcoming blog post on TDD for beginners - Part 2.
Sign up to our newsletter and receive a monthly update of new blog posts.