There are times when I try and convince myself that unit testing my verification IP may not be necessary. Believe it or not, I can put together a pretty convincing argument. On a good day I shake it off and do the right thing. Other times I’m just convincing enough. I know it won’t end well, but I skip the unit tests and go straight to the code.
Here’s a couple arguments I’ve used on myself to get into trouble.
I Probably Don’t Need to Understand this Legacy Code
It may not be the obvious place to start, but I happen to think legacy code is a great entry point when it comes to unit testing. If you’re new to it, unit testing legacy code can help you understand what you’re working with. You can start small by picking out a specific function, read the code to get a feel for what it does, then write and run some unit tests to validate your understanding.
To modify legacy code, I write unit tests around the portion I’m expecting to change. That locks down the legacy behaviour. From there, if there’s an existing feature that needs to change I modify a unit test to capture the change then update the code to match the new test. I try and make my changes 1 test at a time to transform the code a baby step at a time. Similarly, if there’s a new feature I write a new test then the code to match, 1 test at a time.
To me, that’s been the right way. But I’ve also done this the wrong way…
The wrong way starts with the idea that any changes I make are going to be small enough that I can fake an understanding of the legacy code. But the first small change may not quite be enough; maybe I just need to tweak this over here, another minor tweak over there. Add the small changes up and it’s not long before I have code that neither I nor the person that originally produced the legacy code understand. People keep finding little problems; I keep patching those problems. Sometimes patches turn into the right behaviour, but most often they don’t. When they don’t, I turn back the clock to somewhere near the beginning, I write unit tests around the original code I changed, update tests, update the code, etc. Basically, I go through all the steps I should’ve gone through in the first place but only after I get it all wrong, wasting my time and the time of others in the process.
This Code is Too Simple to Test
There’s the code that’s too big or complicated to want to understand with unit tests. Then there’s the opposite problem: I’m about to write code that’s too simple to test. I find this situation more tempting than the legacy code situation because I feel like I’m in control of the unknowns. I forego TDD (my usual tactic) and just spit up the code. If it compiles it probably works, then I move on.
Note that the too-simple-to-test problem can be some new standalone chunk of code. It can also be a simple feature within a more complex component (this is more common for me); I use TDD for most of it but skip a few tests here and there when I think they’re not necessary.
But they’re usually necessary.
For me, skipping unit tests is a careless way to write code. Even if I get it right 9 times out of 10, the 10th time can lead to enough wasted debug effort to more than erase the time I saved by avoiding unit tests in the first place. Maybe some of the simple, tedious unit tests don’t add a whole lot. But some of them add way more than you think and it’s hard to tell the difference as you’re writing them. Safe thing to do is to take your time and write the tests.
There are other arguments to avoid unit tests but these are the two that I still seem to fall for. Simple new code, complex legacy code, it doesn’t seem to matter to me: I can make mistakes just about anywhere! As hard as I try, I can’t avoid unit tests. Sooner or later they all seem to get written anyway.