Test-Driven Development, or TDD, is a discipline that advocates writing unit test cases before any production code.
While some influential software community members are fully committed to TDD, others have declared it dead.
This article examines Test-Driven Development from micro and macro point-of-views.
The examination at the micro level will allow us to see the immediate advantage (as well as caveats) of TDD. These sections look at how TDD is supposed to work, what challenges it faces, and what opportunities become available.
On the other hand, the macro examination will show us how instrumental TDD can be when trying to improve your SDLC processes, especially when optimizing resource usage and shortening the delivery timeframes while preserving quality.
Examining TDD would be part of an overall effort to achieve Operational Excellence, the term that combines quality, efficiency, and flawless execution.
3. TDD – The Micro Examination
3.1 What is TDD
A Word of Caution on Unit Testing
TDD practices often include writing unit tests as the primary type of software testing tool. We have argued at length about the utility, or lack thereof, of unit testing in certain situations, particularly when the “unit” is very small (class or function level).
For this article, when we use the term “unit testing”, we generally refer to either Unit Tests or other types of tests, such as System Integration Tests, depending on applicability.
I found Bob Martin’s definition of TDD in the form of three laws quite useful. This is what we will be using here as well:
You are not allowed to write any production code unless it is to make a failing unit test pass.
You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures.
You cannot write any more production code than is sufficient to pass the one failing unit test.
3.2 Advantages of Using TDD
TDD presents three distinct advantages.
#1 Avoid Wasting Effort Debugging Your Code
Debugging is an effort-intensive and time-consuming process. You should not do it if you can help it.
This is a direct implication of the three laws where you are not allowed, by design, to write any code that needs heavy debugging.
The rationale is as follows: if your code was working one minute ago, all you have to do to debug it would be to undo those last few changes. You don’t have to use the debugger at all.
Granted, that does NOT mean you will never ever have to use the debugger; it just minimises usage.
#2 Decent Battery of Tests
You have completed writing your production code and now have decided to write some unit tests because only it’s part of the process. Your heart will not be into it (since you know the code is already tested and working). TDD helps you avoid that.
A battery of incomplete test cases, full of holes, or with less-than-ideal test coverage adds little value to your development practices. Passing those tests means very little.
On the contrary, having a decent battery that can give you enough confidence to release into production is essential for improving your Agile practices.
Another advantage of decent testing capabilities is keeping your code clean through regular refactoring exercises.
#3 Designing Testable Code
Using TDD can push developers to design and develop code that can be easily tested. This is somehow a natural consequence of the three laws of TDD.
3.3 Challenges of Using TDD
TDD, when incorrectly implemented, will present some daunting challenges.
#1 Cost of Ownership
A growing body of code, perhaps a few times the size of the production code, whose sole purpose is to test another body of code, will introduce a hefty price in terms of cost of ownership.
Even when the unit tests are exquisitely designed and extremely robust, they still require effort to create and maintain. This can be redirected to develop code customers are happy to pay for.
The outcome of Test-Driven Development, when coupled with badly designed software testing strategies, is a body of test scripts that nobody wants to maintain. This can become a major drag on your project momentum, i.e. lower-profit and slower time-to-market.
A poorly-implemented test strategy can slow you down considerably. You can check the list below for signs of a crappy implementation:
- Slow execution. Ideally, a full test suite should complete in a few minutes. Otherwise, it’s unusable.
- Testing implementation rather than functionality. This happens when you couple code designed for testing your software with code from production.
- There is plenty of mocking (ideally, you want to run a test on an environment as close to production as possible). To make things worse, mocking seriously couples your test code with the production one.
3.4 Taming the Challenges
You can engineer your test strategy and automation framework smartly to mitigate these problems.
The following guidelines for implementing a powerful test strategy can be applied:
- Use unit testing ONLY where applicable. This means redefining the “unit” in “unit testing”.
- Avoid mocking (databases, external systems, filesystems, etc.) at all costs.
- Test functionality, never implementation. This is otherwise known as Behaviour-Driven Development.
- Design a simple, robust, and automated framework leveraging existing tools on the market. Look at DevOps practices for inspiration.
- Design code that can be easily tested (more on this later).
In summary, the challenges presented by poor implementation of TDD are neither unavoidable nor unsurmountable.
4. TDD – The Macro Examination
Test-Driven Development is a discipline that will allow us to glue together some practices designed to reduce waste in the development and testing stages, waste reduction being the number one target of Operational Excellence.
The next subsections will explain how this can be done.
4.1 Streamlining Development and Testing Efforts
The traditional approach to testing new code invariably involved QA having to wait until all the development (and its testing) has been completed before carrying on with any of their activities.
It is true that during this time, testers usually occupy their time preparing test plans, perhaps even writing down (or creating) the specific test cases.
All that is slow admin work, and times are much less stressful until testing starts.
The sequential nature of development and testing can be a major source of waste, and the only way to eliminate it is by creative redistribution and reorganising testing activities.
This is where TDD comes in. Have a look at the below diagram.
The solution we propose is as follows:
Once the internal and external stakeholders sign off on the business requirements, developers and testers start writing test cases. Test cases from both parties are integrated seamlessly, typically through Source Code Versioning tools.
Developers using Test-Driven Development practices start writing production code and using the test cases generated in Stage 1 to validate their changes. Testers simultaneously enhance the test suites created by the developers and integrate them.
The production code is finalised at the end of the development cycle, and all the test cases are now passing. The Development AND Testing stages would are simultaneously concluded.
4.2 Embedding Quality Controls in the Delivery Process
Operational Excellence involves a relentless drive for better-quality products. This squares nicely with the way Test-Driven Development works.
To illustrate how that works, here is a small example.
You can include a step in the development and testing processes that says the following: Once a bug has been found, it is required that the developer or tester who uncovered it add a test case that tests that specific scenario.
The developer can then patch the code to make that test case pass. Once it’s done, there is no need to test it again.
A release can be deployed into production ONLY when it passes the full suite of regression tests, and your test framework should provide a report to that effect.
4.3 Continuous Improvement
The first version of any code that you write is usually crap. Everybody knows that. Everybody acknowledges the necessity of getting things done, so you can get away with it the first time.
Sometime later, perhaps when trying to extend that code, you think there might be a better way of writing it. This process is called refactoring and is really what keeps your code healthy.
No developer will have the courage to refactor any code unless she is confident that there is a battery of test cases that can easily uncover broken features.
Continuous improvement requires confidence to be present, and the latter depends on how well you apply TDD.
Test-Driven Development can be a powerful discipline with the right tools when applied properly.
TDD is essential for achieving Operational Excellence by A) ensuring an optimal distribution of resources, B) making the most of their limited time, and most importantly, C) staying on top of your quality issues.
DO NOT religiously follow procedures (such as testing internal classes and mocking databases) invented later on by people who perhaps did not understand the author’s views as he meant them.
These serve no purpose but to slow you down until you finally give up on TDD.
6. Further Reading
Two great talks on the subject.