How to Start Test Driven Development
Have you heard about Test Driven Development but not tried using it yet? Here is a quick introduction to help you get started.
Test Driven Development (TDD) has been around for multiple decades and I began using it as soon as I learned how, but I remain surprised that many software developers, especially those early in their career, do not seem to have heard about it or have been unable to incorporate the practice to their coding.
How to Get Started?
There are many benefits to using TDD, but let’s just get started. This post is going to describe the process of writing tests for a new piece of code. Adding tests to existing code is a little more complicated, as the code may not be written in a testable way, but keep an eye out on this site for some hints and tips in a subsequent post.
Note: you will find many tutorials out there on how to get tests set up for your language, build system and framework if you don’t already have them. It’s beyond the scope of this post to go through the myriad of combinations.
To start, before writing any code, you have some requirement of what the code is going to do. Let’s start with something simple like adding two Roman Numerals together. If I try to add IV + XI
then the answer should be XV
.
Let’s assume that we already have a couple of functions. One that can be used to convert a String
representation of a Roman Numeral to an Int
and another that can do the reverse, change an Int
into a Roman Numeral String
. There are no tests for these so we are not even sure that they will work. In the next post we will begin to look at how to add tests to existing code.
Make Some Assumptions
We can start with assumptions about what the function we want to write is going to look like – note that the assumptions can evolve as we write the tests and the code. Let’s assume that the Roman Numerals are going to be passed as String
s to the function and a String
representing the sum is going to be the result. We probably have to convert the input to integers before adding them. Then we have to convert back to Roman Numeral before returning the result.
In short the code we need will have to:
- convert a
String
representation of a Roman Numeral to an integer. We can use the existing extension function onString
calledfromRomanNumeral(): Int
. - provide a function to add two Roman Numerals together. This is the code we will write using TDD.
- convert an integer to a Roman Numeral
String
. Again let’s use the existing extension function forInt
calledtoRomanNumeral()
.
Write the Test First
The key is to start with a test. You may not even know what to call the actual class or function, or what package it should be in at this stage. Make a good guess and get started. The code here is Kotlin but the approach should be similar for any language.
Note: I like to name my test function to start with should
to indicate what is under test. Kotlin allows the use of back-ticks to allow function names to be English sentences rather than camelCase or snake_case, making them a little easier to read.
@Test
fun `should add two Roman Numerals together showing the result as a Roman Numeral`() {
val four = "IV"
val eleven = "XI"
val expected = "XV"
val result = TODO()
assertEquals(expected, result)
}
Notice that the function call to add two numbers is missing – in its place is a call to the placeholder function TODO()
. This Kotlin feature allows us to defer writing the code to execute the test until the code is created. This test will compile and run but result in a test failure due to an exception when executed. This is especially useful when using TDD as it allows you to postpone writing one part of the code until another part is written.
Create the Function Placeholder
There is no function yet for adding two Roman Numerals, so let’s add it now, but leaving the implementation until later. The code is going to use Kotlin’s object extension capability to add the function addRomanNumeral()
to the String
class. It will return a String
representing a Roman numeral.
fun String.addRomanNumeral(): String = TODO()
Again the use of TODO()
allows us to defer the writing of the implementation. But now the test can be completed.
Complete the Test
Replacing the TODO()
with the function call gives:
val result = four.addRomanNumeral(eleven)
The test will still fail as the function under test has not been implemented. Further tests can be added at this point to cover all the edge cases. For example, it would be wise to test various combinations to stress the implementation. Since the code is going to be using existing functions, care should be taken not to try to specifically test those methods through the tests for addRomanNumerals
. In the next post we will return to adding tests for the existing functions.
Implement the Function
What you should have so far is one or more failing tests that cover any scenarios that you have identified. Next step is to implement the function. Practically all IDEs or build systems have a way to run the build including all tests. Your build should be failing at this point with the number of failing tests indicated.
Without going through the meaning of the following code, here is a possible implementation – I’ve broken it down onto multiple lines for clarity:
fun String.addRomanNumerals(another: String): Int {
val thisValue = this.fromRomanNumeral()
val otherValue = other.fromRomanNumeral()
val sum = thisValue + otherValue
return sum.toRomanNumeral()
}
Run the Tests
Here is the bit that I like the most about TDD. When you finish implementing a function, run the tests and see them all go from failing to passing. This is usually accompanied with a little green tick in IDEs such as Eclipse or IntelliJ.
If all the tests pass, you can commit you code and start on writing the next test. This could be to strengthen the test coverage or to begin writing a new test for new functionality.
Committing code regularly is a best practice, and a good time to do so is when tests pass. If you are using Git for source control, you can commit locally and only push to the shared repository when all your changes are ready, or multiple commits can be collapsed into a single change. For server based systems such as SVN, you may want to create a branch to allow incremental commits before merging; just make sure not to build up too much change at once.
My Test Still Won’t Pass
Some may argue that adding tests while writing the implementation just creates more spots where a bug can be introduced, thus slowing down development. Software engineering is about managing software that will be deployed for a long time while being able to easily change throughout its life cycle. Sure, there will be days that you feel slower, but I guarantee you will appreciate seeing test coverage on code you wrote several years ago when you return to fix a bug.
Debugging failing code is beyond the scope of this post, but there are a few things to check. First make sure that the test you wrote makes sense. No matter how complicated the code under test is, a well written unit test should be easy to read. There should be setup, test execution and result verification. Ideally someone reading the test can see that the expectations are correct for whatever it’s supposed to do. In our example, this should not be too much of an issue.
Next make sure that the implementing code you wrote is doing what it should do. Using the debugger of an IDE is probably the best way to do this; IDEs can also test server side code by connecting to ports opened specifically for the purpose, but this requires a little more setup.
Last thing to check is whether your code is relying on anything else. In our example there is a dependency on two existing functions, neither of which has any unit tests. What if we have a test case which exposes a bug in either the conversion from or to an integer? In the next post we will explore the next level up from TDD which is making changes to code without unit tests.
Code Examples
This post only includes some of the source code; the rest is available in this GitHub gist, which includes the tests and code for the conversion functions also.
Conclusion
The process for Test Driven Development is straightforward: write the test, create a placeholder for the functionality, finish the test, add more tests to cover different scenarios, and then finish the implementation at the placeholder. This will work for nearly every possible new piece of code you will need to write.
Writing tests with TDD means writing a failing test before starting to write any of the code for the new functionality. It will aid you in writing testable code as it forces you to write the implementation in a testable way.