Writing unit tests has been the cornerstone of ensuring quality in software development for decades. Developers that use unit testing develop code that is higher-quality. However, many developers still struggle writing adequate tests or quality tests. As a result, the software lacks good code coverage (tests for almost each and every line of code). This is where Test-Driven Development (TDD) shines. TDD is a powerful method of writing tests and code. Developers using TDD produce quality software that is simple, easy to follow, documented and fully tested. As a result, the code is “agile”, meaning the development of software can respond to changing requirements or needs quickly.
Test-Driven Development vs. Test-After
In the Agile Developer training course we offer, we ask participants to write a simple program, about 15-30 lines of code, to solve a trivial problem. We then ask them to write tests to cover the code. Most developers struggle to test their code. Why? When they are writing to solve a problem they are not thinking about the tests that should accompany it, and the code can be complex, or they are using poor coding style making it difficult or requiring substantial changes to pass a test. Writing code that is testable also encourages good coding style that is simple to follow. TDD takes a test-first approach to development to overcome these challenges and follows these 3 steps:
1) Write a failing test
2) Write just enough code to pass the test (or all tests)
In the first step of this pattern of development, writing a failing test ensures that the test is valid. In this step the developer is thinking about the intent of the next lines of code they are going to write and explicitly state it in the test. A developer writes and runs a test, and it hopefully fails because the developer has not yet written code to fulfill its requirements.
Next, the developer writes the minimum amount of code to pass the failing tests. After writing the code, the developer runs all the tests (or relevant tests) again and we ensure all tests pass. If any test fails then it becomes readily apparent that the code added has broken some expected behaviour of the software. The failing test is identified and the matching broken code is corrected and the tests are run to ensure all tests pass.
Test-Driven Development and Refactoring
The final step that developers sometimes overlook is Refactoring. Here the developer has an opportunity to improve the code that has been written. The definition of refactoring is to improve code without changing its behaviour or results. That is, if the developer performs any changes in this step they should not cause any tests to fail. A refactor could be changing a variable or function name to help others understand the code, consolidating duplicating code, or simplifying the solution some other way. The developer uses refactoring to make a test more verbose and easy to follow. This step ensures that the code is continuously being improved.
Finally, after all refactoring is completed we run all the relevant tests again and ensure none of the changes affected the expected behaviour of the software, and the TDD steps are repeated for the next part of the solution.
Developers must remember two rules: write no new code until there is a failing test, and resolve the test with the fewest and simplest lines of code.
Test-Driven Development and Agility
In this way as the code is being written, tests are also being written ensuring that every line of code is tested. Developers run the tests continuously as a form of feedback. When there is nearly full test coverage, the tests themselves become a form of documentation for other developers, and a safety net for future changes to the system. Developers can identify and rectify unexpected behaviour of the system quickly and easily, while adapting the software to respond swiftly to changing requirements and needs.