Sergey Nivens - Fotolia
Sometimes, you must fail before you can succeed. There's no better example of that than test-driven development.
Test-driven development (TDD) is a methodology that focuses on creating tests, writing code that passes those tests, and then repeating the process over and over, in that order, to build software or a feature. The tests come first to ensure that the specifications of the project are well understood before developers undertake the task of implementing them.
TDD mainly focuses on unit tests. The specifications are granular; most TDD tests only assess a specific function or object. When the software development cycle starts with tests, it forces the programming team to ask questions and mentally clarify the functionality from the beginning.
An example of TDD demonstrates how the test process works.
There's a classic coding challenge called Fizz Buzz. The goal is to create a function that does the following:
- Print the numbers one through 100.
- On numbers divisible by three, print Fizz instead of the number.
- On numbers divisible by five, print Buzz instead of the number.
- On numbers divisible by both three and five, print FizzBuzz instead of the number.
Let's modify this function to be testable. Rather than print the item, let's have the function simply return the numbers we want. Now, we can test it.
In TDD, I start by writing a test that calls my function and the output should match the specifications. To write this test, I don't need to consider any sort of implementation; the function is a black box. I focus on understanding the most important aspect: the expected output.
In this TDD example, my test calls the fizzbuzz function and expects an array with the correct items defined in the specification. In creating the expected output array, I attempt to understand the intended functionality. I write a test that adheres strictly to the specification. This initial test allows me to immediately know when I have implemented a function that meets the understood specification. It fulfills one of the ultimate goals of TDD: Understand the specification before writing the implementation.
Now that the test exists, it's easy to know what to write for the function. Test the code and rewrite, as needed, until the test passes.
One mistake someone might write for the Fizz Buzz challenge is a conditional statement like:
- If the number is divisible by three, replace with Fizz.
- Else if the number is divisible by five, replace with Buzz.
- Else if the number is divisible by three and five, replace with FizzBuzz.
This statement might make sense upon a first reading of the specification. But, when the test runs, the final else if condition for numbers divisible by both three and five is never reached. The first two conditions catch any number that is divisible by either three or five.
TDD provides instant feedback. In this example, it informs the developer that there is a mistake in the implementation. The test prevents work after code completion to debug the issue where FizzBuzz never appears because the function was incorrect. Additionally, TDD unit tests lend themselves to automation. Validation through automated tests is repeatable and less error-prone than through manual tests.
In this TDD example, our function fails the test, so we must fix it. First, check if a number is divisible by three and five, and then individually check those numbers in subsequent conditions. After the test passes, restart the TDD process.
If the specification has more details, write more tests along with more code until the new tests pass. When all tests pass, refactor the code for readability and performance.
A convenient byproduct of TDD is that the test suite serves as a project specification. The test suite can educate project contributors on the project's low-level functionality. When more coding is required, whether to update a library component or redesign the logic, developers can rely on the test suite to guide them on these updates, showing that existing functionality works.
Red, green, refactor
In TDD, refactoring is visualized by a red, green, refactor loop. This loop is at the core of TDD. Here's how it works:
- When a test is written, it fails (red).
- The developer writes the minimal code to pass the test (green).
- Consider improvements for the overall design (refactor).
You can see this loop in practice in the FizzBuzz example of TDD above. The failed test led to implementation improvements so that the feature works correctly, and the test verifies that it fulfills the specification. You must always pass the test, but might not need refactoring after getting that green light.
Here are five questions to determine whether or not to refactor:
- Are my tests covering all of the functionality expected?
- Are the tests descriptive enough to allow others to understand when there are failures?
- Are the tests independent of each other?
- Are there any places where duplication can be eliminated?
- Can something in my implementation be written more simply, clearly or efficiently?
Answer and implement the answers to these questions to improve software quality and achieve the ultimate goal of TDD. Successful tests lead to quality code in production, which is the foundation of a good user experience.