Are you looking for a programming technique that eliminates bugs, increases test coverage, helps with designing solutions, and provides documentation for your API? Look no further than Test-Driven Development (TDD)!
At Administrate, we encourage our engineers to work using TDD and to leverage the many benefits that it brings with it. This blog post will take a look at what TDD is, the techniques you can employ in order to be successful with TDD, and give a small example of TDD at work.
What is TDD?
Test-Driven Development (TDD) is a software development approach in which test cases are developed to specify and validate what the code will do - not what the code already does, but what it will do. That’s because TDD allows you to write your tests up-front and utilize short feedback cycles in order to help define the solution to the problem you are solving.
TDD originated as part of Extreme Programming (XP) with the intention of improving software quality and helping to facilitate short iterations. XP is an agile software development framework with a strong focus on incremental design, refactoring, and short feedback loops - TDD plays a crucial role in helping to realize these things. The good news is that you don’t need to be utilizing other aspects of XP in order to make use of TDD.
With TDD, three activities are tightly interwoven: testing, coding, and design. At the end of a TDD cycle you should have a suite of tests that document your API and live alongside your production code, helping to mitigate against introducing bugs and regressions in later cycles; clean production code that passes all of the tests in the entire test suite; and a well-thought-out design that solves a real business problem.
A small word of warning if you haven’t worked with TDD before - you will need to make a fairly significant mindset change in how you approach writing code in order to make TDD work. It can take some time and practice to get to a point where writing failing tests up-front is intuitive, but you will reap the benefits once you have mastered the art.
Now we’ll dive a bit deeper into why we actually write the tests before we write production code.
Why write tests first?
How many times have you dived head-first into creating a solution to a problem only to find part-way through that some assumption you made was wrong or that you’d completely misunderstood the problem and need to throw away the work you’ve just completed? It’s frustrating, time-consuming, and detrimental to the customers that are having to live with the problem.
One way to allow yourself to work through a problem more patiently and methodically is to write tests before you’ve written your production code. Doing this helps you to solve the problem as you go and to take stock of what it is you are trying to achieve at frequent intervals. Your initial test can help you to solve just one simple part of the puzzle and then allow you to incrementally enhance your solution until you are satisfied that the code is functioning as desired. Running your tests suite frequently enables short feedback cycles and helps to quickly identify when you have broken the intended functionality as well as when you have satisfied some requirement.
TDD also prevents engineers from falling into the trap of thinking that they can just add tests after the fact. We’ve all had this mindset at some point and almost certainly never found the time to go back and add the tests we intended to. Even if we do find the time, chances are you will have forgotten what the exact test cases were that you needed to accommodate and so end up with insufficient test coverage. At Administrate, we have enforced that our domain code has at least 95% test coverage as part of our Continuous Integration steps otherwise the build will fail and we won’t be able to ship the code change - working using TDD helps us to ensure we stay above this level.
Lastly, tests can act as one of the best forms of documentation of the codebase. The better the tests are at describing the intent of the code, the better documentation you will have against your API. Tests that are created using TDD tend to be well crafted, clearly named, and focus on specific pieces of functionality - this helps to give a clear picture of the intent of the code.
Red, Green, Refactor - the TDD mantra
Red, Green, Refactor is a repeatable pattern that engineers use to facilitate TDD. It is made up of the following 3 steps:Red
Write a little test that doesn’t pass and perhaps doesn’t even compile at first.Green
Quickly make the test pass, committing whatever sins necessary in the process.Refactor
Eliminate all of the bad code and duplication created in merely getting the test to work.
Working this way ensures that you know the problem up-front (i.e. you have a failing test that you need to fix) which helps to focus your mind on finding a solution. This solution doesn’t need to be the most elegant solution during the first Red, Green, Refactor cycle - you just want the positive affirmation that you know how to solve the problem. Once you have found a solution (i.e you made the test pass) you can take a step back and improve your solution through refactoring - all along continuing to run your tests to ensure that you have not strayed away and made the tests fail.
Let’s run through a quick example of TDD in action, making use of the techniques we’re just learned.
TDD - an example
We’re going to create a Shopping Cart that satisfies some very simple requirements:
- The Cart should be able to hold Items.
- It should be possible to check the total quantity of Items in the Cart.
It’s important at this stage that we consider the Red, Green, Refactor mantra and don’t dive head first into the implementation.
We want to satisfy the first of the requirements, but we also want to start off by creating a failing test.
We know that we need a Cart, so let’s create a very simple test that instantiates a
Cart object and asserts that it isn’t
The test fails, but we expected this as we haven’t even written a line of production code yet that defines a Cart. This is the red part of the cycle.
Let’s fix this up so that we have a passing test. At this point we want to do the minimum possible in order to achieve that:
We have added a
Cart class but not specified any of its properties - this is fine as we’ve done all that we need to do to get the test to pass. This is the green part of the cycle.
Now, let’s perform a simple refactor without causing the test to fail:
We’ve moved the
Cart class out into its own file so that we are separating the test code from the production code and we’ve still got a test that passes. This is the refactor part of the cycle.
So far so good. However, we haven’t actually satisfied the requirement, so let’s go through another Red, Green, Refactor cycle to get us closer to that point.
We know we need the
Cart to be able to hold Items, so let’s enhance the test so that it has a more suitable assertion that we know will fail at this point:
Now we need to add an
items attribute to the
Cart class so that the test passes:
At this point we have a passing test and we can be fairly satisfied that we have met the requirement. We don’t have to perform the refactor part of the cycle for every iteration if there isn’t anything worth refactoring, so let’s skip it on this occasion.
Our solution is very basic at this point and we’ll probably enhance the production code as we go, but we’ve worked through the initial problem and have formed a clearer picture of what the Cart will look like.
Now we can move on to the next requirement by testing that we can check the total quantity of Items in the Cart.
We start another Red, Green, Refactor cycle with a failing test:
Again, we need to enhance the Cart class in order to get the test to pass. Let’s implement the most basic thing we can to get the test to pass:
The test now passes but we have resorted to hard-coding the return value of the newly added
quantity property purely to make the test pass.
At this point we can refactor the code so that it’s something we are a bit more proud of:
In this change, we have made the
Cart a bit more sophisticated. We can pass a list of
items to the
Cart which will become instance variables and we have added
quantity as a property, calculated as the length of
Oh no, it looks like we have made one of the tests fail as a result of this refactoring! This isn’t cause for panic, though - these short feedback cycles provide us with the opportunity to adapt and react quickly to failures. In addition, we know that we have enhanced the production code, so we probably just need to give a bit more attention to the tests themselves.
The signature of the
Cart class has changed, so let’s make sure we are instantiating it correctly in the tests:
Great, we are back to having both tests pass, however, We have introduced some duplicate code in the tests, so let’s go ahead and refactor that so that the code is cleaner:
Here, we have extracted the duplicated lines of code that were instantiating
cart into a
setUp function that we know will be run prior to each test, allowing us to share the setup between both tests.
If you’ve gotten this far, why not see if you can enhance this code using TDD in order to satisfy some further requirements?
- It should be possible to check the total price of the Items in the Cart.
- It should be possible to assign Contact details for the owner of the Cart.
- It should be possible to progress the Cart through various stages of a Checkout process.
Hopefully, you have reached the end of this article with a desire to tackle your next project using TDD!
TDD will help you to write more focused tests, increase your test coverage, write better production code, and help with your design thinking.
It takes a bit of time and practice to get into the right mindset to make TDD work, but once you do you will be grateful for the many benefits it brings.