Let's say you have a project to deliver feature XYZ. After analysis, design and planning, you came up with a term of 2 months: 2 weeks for scoping a work, 4 weeks for coding and 2 weeks for testing. Sounds great.
As anything else, testing has multiple perspectives. The most obvious one is verifying results of your work and keeping quality under control. The other one, less obvious, is minimizing the risks caused by numerous bugs. Third one is related to previous two: minimizing the cost of software development and support. The earlier you test, the easier it is to fix and less bugs you have later.
Lets look at two possible scenarios:
Scenario 1: No unit tests
So you don't believe in unit tests or you just don't have a time to write them. You'll better test everything manually during 2 weeks of test phase.
You have finished with your code, and now it is time to wire everything up and start testing. Oops. You found a small bug, where you just had forgotten to add "!" in your if statement. Quick fix. Easy peasy. You make a change. Your favorite build tool picks your last commit and makes a new build. After 10 minutes you have a binary. Another 5-10 minutes and you have it deployed and ready for testing.
You test it again, and... Oops. You found a small bug. Looks like you actually need to call that method first, otherwise you get NPE. Damn. What a stupid mistake?! Quick fix. Easy peasy. You make a change. Your favorite build tool picks you last commit and makes a new build. After 10 minutes you have a binary.
You test it again, and... Oops. You found a small bug. Well, I guess you see where I'm heading. 2 small stupid bugs, you already spent 1 hour. You have only 2 weeks, but you can’t even get it working for happy case, don’t mention for all other dozens of test cases.
And nothing prevents you from regressions during bug fixing.
You probably see now, what is wrong with this scenario. You think you've saved time during coding, but actually
you didn't.
Unit tests are a convenient way to spread testing work throughout development phase. So you'd need to do less during testing phase, where fixing bug is already an expensive thing.
I rarely find bugs when I write unit tests. Maybe 1 in 20 tests that I had actually found and fixed a bug. But, man, this is so fast to fix a problem at this time. Everything is locally on my machine in my IDE. I don’t need to build and deploy change, neither others are blocked by me. Found, fixed, done! A tad of time here, saves hours later!
Scenario 2: Write unit tests during coding
So by now you've decided to try out writing unit tests. Maybe this will help to avoid the hell you've had during Scenario 1. You start writing unit tests for most of the code you produce. It has been really hard at first, but later became easier. You use mocking a lot and thus unit testing becomes almost painless. Actually, you can't imagine now how to write new code without tests, but time to time you skip writing tests for less important parts.
You've finished you coding by now, and ready to start testing. And you find that there are not that much bugs now. And when you find a bug, you know how quickly to fix it and verify with unit tests. Thus, you don't need to go through tedious fix-build-deploy-verify cycle as you used to do during Scenario 1.
As result, you spend more time during coding phase, as you need to write unit tests. You however spend less time during testing phase, as you can iterate bug-fixing faster.
Of course, you still spend a tad more time than planned: but it is because you need to learn how to estimate better!
TDD vs Plan Ahead
Assume we have 3 approaches:
- you follow TDD and write tests first, and then write code to make your tests pass eventually
- you take a paper or text file, and you plan ahead by writing a list of test cases that need to be converted to automated test suites; then you code and write test cases during coding or afterwards
- you don't plan what test cases you have, you just write a code and cover it sporadically with unit tests
It is probably clear that 3rd approach is the lacking one here. You start writing code without defining what are your expectations. You don't have a plan of what needs to be tested. As result, it is easy to miss many important test cases.
1st and 2nd approaches sound very similar, as they both have the same goal: know and plan what you're going to do before you start coding.
However, 1st approach forces you to build constraints for you and your code before you start coding. Of course, you can refactor those later, again and again. While 2nd approach gives you a flexibility to write the code and refactor it until it fits your vision. You can now create tests during or after you've finished with piece of code.
Both 1st and 2nd approach require self-discipline, but in different ways. TDD needs a discipline to start and keep to using it. 2nd approach needs a discipline to convert test cases on paper to unit tests in code.
Not obvious difference between 1st and 2nd approaches is completeness: it is easy to miss something when you write a code. If you start defining a list of test cases on paper, it is always easier to identify completed list.
Summary
Here are some bullet points as a summary from this post:
- writing unit and functional tests is important to keep quality under control, save time and money
- it is never too late to write tests for the code, but now is better than later
- thus, write tests during coding and before commit and code review
- writing tests before code does not make things cheaper or better, unless you don't have a discipline to cover functionality with tests
- we need to work on improving our estimation skills.