5 Ways Test-Driven Development Benefits Your Projects
For the past several years, I've worked mostly on teams following Test-Driven Development (TDD) practices. In that time, I have come to realize the many benefits that TDD carries with it. I've also come to appreciate how it has improved my development workflow - in fact, it's become so ingrained in my day-to-day process that I now find it painful to work without.
Although I clearly see the benefits, I've noticed that they're somewhat obscure to both management and dev teams who are considering adopting TDD. In my experience, this lack of clarity leads to concerns such as:
-
Management:
- What impact will this have on feature implementation / delivery?
- Will we need to increase staffing?
- Isn't this why we have a QA team?
-
Dev:
- This looks like a lot of duplicate effort.
- How will this affect my dev cycle and pace?
- What about the learning curve for the new technologies and practices?
Honestly, these are all pretty good questions, so I thought I would write an article explaining how my projects and teams have benefited from TDD:
Reduces Risk
Although risk (in the sense of "Chance of failure in a particular endeavor") first seems to be mostly a management concern, in fact it affects the entire team. A simple example would be a pre-release bug bash that exposes a showstopper bug caused by a deep-rooted architectural flaw, thereby delaying the release - that's a pretty major risk (the possibility of unplanned release delays) to take on in a commercial product. Fortunately, TDD can help mitigate these risks by ensuring that each component functions as expected in isolation (with unit tests), and also that subsystems interact with one another correctly at runtime (via integration tests).
TDD also can have a positive effects on productivity because it dramatically shortens the feedback cycle between introducing a bug and fixing it - i.e. the developer notices the bug because one or more tests fails, and then fixes it so the tests will pass again. This, in turn, leads to a reduction in bugs 'leaked' to the QA team, reducing their burden and freeing up time to work on tasks such as test automation and integration testing.
Another, more dev-related benefit to TDD comes from using unit tests as part of a 'preflight' checklist before committing code to SCC. On some team projects, it's fairly common to get the latest codebase and find that someone committed a change that breaks something, thereby interrupting development until the broken code is fixed. With appropriate test coverage and a preflight checklist, these issues all but vanish. My usual workflow is to run the full unit test suite immediately before checking in, and immediately after getting the latest codebase.
Encourages Clean Coding Practices
On the other hand, clean coding at first seems to be primarily a dev concern, but in fact also benefits management, particularly as applications go into longer-term maintenance. Code written according to SOLID principles is substantially more flexible and maintainable, enabling rapid release iterations and consistent feature releases.
I have found that TDD excels particularly at encouraging both Dependency Injection and Composition, and can even help guide your implementation. For example, if you need to test a complex internal function (i.e. a private method, etc.), that may be a hint to extract that function out to a separate, more testable class, and inject it into the original class as a dependency, allowing you to mock the internal behavior.
Another benefit offered by TDD is an emphasis on refactoring - in fact, one shorthand mnemonic for the TDD workflow is "Red, Green, Refactor". This focus on continuous improvement of the codebase generally leads to a cleaner, more architecturally-sound (and hence more maintainable) implementation.
Better Documentation
I have some pretty strong negative opinions on the value of "documentation" in code, for example XmlDocs - They produce a 'feel-good' sense of "Well, my code is totally documented!" while presumably fulfilling some process checkbox, but in my experience the value of these comments is questionable, for example:
<summary>
Does some work
</summary>
<param name="foo">The foo</param>
<param name="bar">The bar</param>
public int DoSomeWork(IFoo foo, IBar bar)
...
In this all-too-common example, the XmlDocs don't particularly add any value - they're just parroting the method signature in XML. They also really don't tell me anything about what this method might be doing with foo and bar, or what might happen under exceptional cases. Worse yet, eventually someone will come along and change the signature or behavior and forget to update the XmlDocs, so they're now out of sync with the actual implementation - that is to say, they are lying to you.
On the other hand, TDD gives you a rich suite of usage-focused documentation, telling you not only what dependencies exist, but how the system interacts with them, along with expectations of how the feature will function under a potentially wide range of runtime scenarios, for example:
// Pseudocode, assuming there are mocks of IFoo and IBar, we've created a SystemUnderTest,
// and this is all in a test fixture of some sort...
[Test]
public void DoSomeWork_WhenFooThrowsException_DoesNotThrow(){
// ... set up MockFoo to throw an exception ...
Assert.DoesNotThrow(()=>SystemUnderTest.DoSomeWork(mockFoo.Object, mockBar.Object));
}
[Test]
public void DoSomeWork_Always_PerformsExpectedWork(){
// ... set up mockFoo and mockBar expectations, return values, whatever ...
// ... These setups tell us what "Expected Work" means (from the method name) ...
SystemUnderTest.DoSomeWork(mockFoo.Object, mockBar.Object);
// Verify that our mock expectations were met
mockFoo.VerifyAll();
mockBar.VerifyAll();
}
[TestCase(Int.MinValue)]
[TestCase(42)]
[TestCase(Int.MaxValue)]
public void DoSomeWork_Always_ReturnsValueFromBar(int expectedResult){
// ... set up mockBar to return expectedResult ...
var result = SystemUnderTest.DoSomeWork(mockFoo.Object, mockBar.Object);
Assert.That(result, Is.EqualTo(expectedResult));
}
As a dev, this documentation is far more useful because it shows me how to actually use the DoSomeWork method, while also showing me how it behaves under various scenarios (i.e. if one of the dependencies throws an exception). More importantly, this documentation will not go out of sync with the codebase, because if it does, the tests will break.
Management can also benefit indirectly from this type of documentation, because it can be used to rapidly onboard new dev team members, helping to mitigate any effects related to team churn.
Focus on Small, Granular Changes
Another benefit I gained from adopting TDD was a significant improvement in my problem decomposition skills. TDD encourages developers to think about small, discrete aspects of their implementation rather than a complex, amorphous whole. This shift in emphasis makes it much easier to adapt 'midstream' if it becomes clear that a particular approach won't be suitable.
In time, I also found myself changing the way I thought about my code overall; My focus began to be less "How should I write this?" and more "If this already existed, how would I test it?". After making that mental shift, it became increasingly easy to answer the question "What should I test?" because I was thinking in terms of code flows and dependency interactions instead of writing large, complex, 'Test everything at once' test cases.
I assert that this is one of the most fundamental benefits of exploring TDD, even if you choose not to use it by default - the decomposition skills will still translate into improvements in your implementation code.
Peace of Mind
In my opinion, this is the most valuable benefit of all: The ability to go to sleep at night and know that I will not receive a phone call at 2AM from the offshore QA team because they're blocked testing a feature due to a bug that should not have leaked past dev.
I also like that TDD gives me the confidence to say "Bring It. Do your worst." to testers during prerelease bug bashes because my test coverage probably already covers the most likely execution and behavior issues. Best of all, if a bug is discovered during the bug bash, I'll just write a new test to repro the bug, make changes to fix the bug, and then prove it was fixed once that test passes (plus, I've updated my documentation to account for a new execution scenario - Score!).
As with the other benefits, this one also applies to Management, who can take the same confidence in the codebase and use it to refine delivery estimates, budget, and feature load so they can sleep at night too.
Conclusion
As should be clear by now, I'm definitely a TDD advocate. To me, the difference is so clear that I think of my career as "Before TDD" and "After TDD", and I must say that the "After" phase has gone much more smoothly.
After going through this, we can now provide preliminary answers to the Management and Dev questions from the beginning of this article:
-
Management:
-
What impact will this have on feature implementation / delivery?
- It will slow things down somewhat at first, but once some infrastructure is in place and dev is up to speed, momentum will build to a faster pace than before.
- It will improve your ability to estimate delivery dates, reduce bug counts, and stabilize a regular release schedule.
-
Will we need to increase staffing?
- That's more a function of your feature load than TDD. In most TDD shops, the implementing developer is responsible for writing the tests, so you wouldn't need to hire a dedicated "Test Developer" or something.
- If you're adopting TDD 'cold', i.e. there are no resources on the team with a TDD background, it may be useful to hire a consultant to introduce the concepts and general practices to the team in order to mitigate ramp-up time.
-
Isn't this why we have a QA team?
- TDD doesn't replace a dedicated QA team, instead it augments it to add another layer of risk mitigation to the project.
- Under certain circumstances, the QA team may even take responsibility for writing certain types of tests to automate repetitive UI-layer testing or the like.
-
-
Dev:
-
This looks like a lot of duplicate effort.
- While it's true there TDD introduces some overhead, a lot of that effort takes place during the ramp-up phase and diminishes rapidly afterwards. Strategic architecture choices (i.e. base test classes, etc.) can mitigate this effort further.
- For me, the hardest part of adopting TDD wasn't the extra work, but getting my head wrapped around the "How do I test this?" question. Once I had reliable patterns in place for testing common scenarios, the additional effort was trivial.
-
What about the learning curve for the new technologies and practices?
- It's true that there is a bit of a learning curve associated with TDD adoption - there's testing frameworks, mocking frameworks, common test patterns, and possibly even new concepts (Mocking was completely new to me when I started TDD). The good news is that with a little bit of guidance, these concepts aren't extremely complex to master, so the learning curve, while real, has a relatively short duration.
-
How will this impact my dev cycle and pace?
- You'll find yourself focusing more on small, granular implementation details instead of trying to design an entire feature up front. This approach also tends to increase your SCC commit rate due to the small changes.
- Your bug leakage rate should decrease as your unit tests catch more and more error cases before they escape to QA or Production.
- As with any adoption process, things go kind of slowly at the outset, but I generally find that momentum picks up quickly. To put things in a scrum perspective, here's how things went when I adopted TDD (These numbers are averages from a 2.5 year project. "Before TDD" was the first 6 months, "Adopting TDD" was about 8 weeks, and "With TDD" represents the remainder of the project):
-
Stage | Story Points/Day |
---|---|
Before TDD | 0.7 |
Adopting TDD | 0.3 |
With TDD | 1.2 |
Additional Resources:
Here are a few additional resources to use when exploring if TDD is right for your team:
Want to Get Hands-On?
Check out these other CodeMentor articles I've written on the subject!
- Intro to Unit Testing C# Code with NUnit and Moq (Part 1)
- Intro to Unit Testing C# Code with NUnit and Moq (Part 2)
Prefer a 1:1 Approach?
Contact me on CodeMentor to schedule a 1:1 Screensharing session to get you up to speed quickly!