Why Bother With TDD?
Does it really make sense to test code that hasn’t even been written yet? Or to disrupt your development mojo every minute to stop, write tests, and refactor code? It does if you want to deliver software faster, through better code, with fewer defects, and greater agility. As software development leaders, it is important to understand that Test Driven Development goes well beyond quality control.
What is Test Driven Development?
As the name implies, Test Driven Development is an approach to software development through which a design and implementation are derived from writing tests. It forces the developer to construct code that is loosely coupled, and therefore, easily testable. The architecture and implementation evolve from the tests that are written prior to writing the code. For those who have not yet adopted TDD, this often seems to be a very odd and unnatural way to develop software. But as awkward as it may seem, it does work.
TDD was conceived by Kent Beck as an application of the test-first principle found in eXtreme Programming (also developed by Kent). It advocated very short development cycles where a developer is continuously switching between writing a failing test that validates a requirement or design, writing implementation code to satisfy that test (make it pass), then refactoring the implementation and test code. These short stages are often referred to as the Red, Green, Refactor stages of Test Driven Development. Red indicates a failing test, green a passing test, then refactor. It is important that the refactor stage include the clean up of the implementation code, along with the tests.
Uncle Bob Martin created the following three laws of test driven development:
- Write no production code except to pass a failing test
- Write only enough of a test to demonstrate a failure
- Write only enough production code to pass the test
By following these three laws, your system will naturally evolve into one that is easily testable. This alone will greatly improve the quality of your code.
It is no coincidence that Kent Beck, the father of XP and TDD, is also one of the 17 original signatories of the Agile Manifesto. Agile was conceived with XP practices in mind; which is one of the reasons that XP and Agile practices blend together so well. And TDD is no exception. Like Agile, TDD is very much an iterative practice. It facilitates the evolution of software over time. As a result, this integration of practices fosters continuous improvement throughout the development lifecycle.
For those of you who have been programmers for a while, you probably have had the occasional experience of refactoring spaghetti code or code with a considerable amount of technical debt. On rare occasions, leadership will support a dedicated effort to refactor code in hopes of increasing productivity. But refactoring complex code (or even simple code that is poorly written) can be difficult, time-consuming, and risky. When refactoring legacy code, we usually don’t have a good way of ensuring it continues to work properly. We may even end up introducing bugs, which we discover through manual testing efforts. Then we spend considerable time debugging the code, which eats into the time set aside for refactoring. At the end of our time-boxed effort to improve the code, we discover we did not fix as much as we wanted or needed, since we spent so much time debugging. Then as QA members begin running their suite of system and regression tests, they discover even more bugs that were not there before this whole effort began. After a lot of debate and frustration, the decision is made to throw away the refactored code and revert to the original state.
Of course, this is not always the result of a big clean-up effort. But I have worked in this industry long enough to see this play out on more than a few occasions. And once you have had this experience, you will certainly shy away from participating in a big refactoring effort ever again.
This is obviously a big problem that occurs much more often than it should. But we can’t afford to leave bad code lingering, which will only make us go slower as time passes. We can’t be afraid to refactor code for fear of breaking it.
So how do we eliminate the fear of refactoring? By employing Test Driven Development. TDD will result in a comprehensive suite of tests that will ensure that virtually all bugs are caught, can be easily automated and executed at the click of a button, can execute in a matter of minutes, will never get out of date with the system, and will eliminate the fear of changing the code.
Until they get experience with TDD, many developers and leaders think such a suite of tests is too time consuming to create. The fact is they can develop faster, with fewer defects, spending less time debugging, and coding better.
TDD helps you to code better by producing testable code. One of the biggest problems with software is that most of it is tightly coupled, making it very difficult to test. By following TDD practices, you inevitably write loosely coupled code that is easily testable. And not only does that produce testable code, it produces code that is extensible and easily maintained.
Building Code Coverage
It is well understood that if you want to optimize productivity, you must maximize the number of automated tests. But all tests are not created equal. While unit tests can be easily written, quickly executed, and clearly automated, system tests take much more effort to plan and write, and can require a considerable amount of time to execute. Therefore, most of your tests should be unit tests. They should execute in milliseconds and target 100% code coverage. For these reasons, TDD is best achieved by writing unit tests.
Component tests should then be written to validate the behavior of individual components; which encapsulate specific sets of business rules. These should require little effort to write and execute quickly, since they are decoupled from the other application and system components. Component tests should target about 50% of the system.
Integration tests are written next to validate the linkage and communication between components. This also helps to ensure a proper architecture is in place. This should cover about 20% of the system.
System tests are written to validate all layers of the application beginning at the user interface. This type of testing includes, but is not limited to, load testing and penetration testing. Since these are expensive and time-consuming to write and execute, they usually target about 10% of the system.
Manual or adhoc tests are performed by people to explore the application for unexpected behavior. Since manual testing is the slowest and most expensive to perform, this type of testing is best kept to a minimum.
While Test Driven Development provides obvious value in supporting quality control, it’s significance goes well beyond ensuring that all defects are discovered and fixed prior to release. TDD will give your team the courage to refactor any part of the code without fear of breaking it. This enables your team to continuously improve the code base, without negatively impacting your release schedule. The outcome is a loosely coupled code base that is easily maintainable, extensible, and yes, testable; resulting in shorter development time with fewer defects.