It’s been a while since my last piece. I’ve been involved in a rapid development project that has taken up most of my time, but today I got tired of not writing. Let’s dive back in.
Last time I made the following statement:
For my money, comprehensive unit testing, enforced by test-driven development, is hands-down the best investment in software quality that I’ve encountered.
Why is Test-Driven Development (TDD) so effective? Three reasons stand out:
- Unit tests can catch the vast majority of bugs
- TDD is far easier than writing unit tests after coding
- TDD enforces good architecture
Let’s deal with the first two.
Unit tests can catch the vast majority of bugs
I’ve written before about my taxonomy of embedded software bugs.
Think of all the code that exists in a typical embedded application:
- If a piece of code directly interacts with hardware, then it may contain hardware interaction bugs.
- If a piece of code interacts with another task, thread, or interrupt handler, then it may contain concurrency bugs.
- If a piece of code coordinates with another external system (especially one running code written by you or your team), then it may be the source of system interaction bugs.
- All other code is what I refer to as “business logic”, and is therefore subject to business logic bugs.
I’ve found that any single business logic bug is typically not that difficult to find or fix when it is observed in testing. So, the inherent difficulty in preventing business logic bugs is that there is so much code that could be buggy.
What to do?
I’ll spare you the suspense: Unit testing!
All business logic bugs can be caught by comprehensive unit tests.
Let me say that again.
All business logic bugs can be caught by comprehensive unit tests.
I challenge you to send me a counter-example. (Please do! I love being proven wrong. It makes me more right the next time. But I don’t think I’m wrong about this.)
Writing unit tests as you go is much easier than writing them after-the-fact
Writing comprehensive unit tests after-the-fact rarely works.
What do I mean by comprehensive unit tests? To be truly comprehensive, no behavior is untested.
This happens automatically by following two of the key tenets of Test-Driven Development:
- “No code without a failing test.”
- “Write only the code required to make the test pass.”
If you try to write lots of unit tests after your code is complete, I can guarantee you that it will take far longer than if you’d written them as you wrote code. More likely, the tests you write will either not be comprehensive, or you will get so frustrated by the effort that you will give up.
Write tests as you write code. This practice is formalized in TDD. Follow TDD, and you can’t help but write comprehensive unit tests, preventing large swaths of bugs along the way.
Be safe out there, and happy developing.