I’m going to go out and say it. I love unit tests. Hopefully, by the end of this post, you will, too. I’m not going to cover the pros and cons of various frameworks in each language, the best way to mock out data dependencies, or even the secret to writing impeccable unit tests. There are enough resources, and I hope that you’re inspired to seek them out. In case you aren’t convinced that unit tests aren’t the best thing since sliced bread, I’ve brought my old friends from Office 97, the Screen Beans people, to help me.
What’s a unit test, anyway?
A unit test is a little snippet of code that one writes to verify that some functionality one just implemented works as expected. This test is never run in production, just when you want to confirm that some code you wrote before still works.
As an example, say you wrote this awesome function in Python.
return isinstance(thing, Apple)
You could write a corresponding test for that function (using Python’s unittest module).
myApple = Apple()
myOrange = Orange()
If your isAnApple() function works, testAppleChecker() passes. If it’s broken, testAppleChecker() fails. Easy peasey, lemon squeezey. You can even write a nice little script that runs all of your unit tests. Or even a build rule that won’t deploy your code unless all unit tests pass! But I get ahead of myself.
But I just wrote all this code. Why do I need to write a stupid test for it? It’s such a waste of time.
Whoa there, buddy. Calm down. You’re right. In my example above, I used 4 lines of testing code for the 1 line of actual code I wrote. And what if I decide I don’t actually want isAnApple() anymore? Or if I refactor it away? Not only did I just waste all that time writing my unit test, and I’m going to waste even more time getting rid of it!
The truth is that the extra few minutes you spend keeping your unit tests up-to-date aren’t going to impact your productivity all that much. As it turns out, for most developers, the majority of their time is spent thinking about the problem and designing a solution, not actually typing it out. In general, writing tests for a chunk of code requires less thought than writing the code in the first place, and you should know how that code is meant to behave, so the tests should almost write themselves. But in case you still think that your time is better spent on Reddit, I’ll list the Six Awesome Benefits of Unit Testing.
Benefit #1: Forcing yourself to write testable code
Decomposition, breaking your problem into small pieces, is one of the first lessons in an introductory programming course. Incidentally, it’s one of the first lessons forgotten. You write a function, and, as it needs to do more, you keep bolting on functionality to it until it gets unwieldy. For example (again in Python), you start with something like this:
do something great
And before too long, it ends up like this:
cfg = getConfig()
do something awesome
do something else
ok one last thing
Your unit test, covering all possible execution paths of f(), gets even more noodly, and you can’t quite be sure if it even covers all the cases it needs to. But because you’re Captain Unit Test, you break up f() into more manageable, testable pieces. Your code is more readable and easier to test for correctness. Everybody wins.
Benefit #2: Faster development iteration
Without unit tests, a typical development flow on a web application might go something like this:
- Work on some piece of your application.
- Fire up the server with a new build.
- Try to access the application in some way that uses the code you just wrote.
- Lather, rinse, repeat as desired.
Depending on the extent of your application, compiling and running your code may take some time, and it’s not always easy to prod your application in just the right way to be sure that it’s correct. In some cases, testing each use case may involve restarting your server to give it a clean slate.
In contrast, you can write a unit test or two to cover each use case. Instead of running the whole server, you can just run those tests and be guaranteed that it’s tested exactly as you specified. And even better, when you write a new feature, you don’t have to go back and test every previous feature. Assuming you write good unit tests, you can save time and just run the test suite. So, all that extra time you spend writing tests in fact pays off as you iterate on your application!
Benefit #3: Built-in examples
So, you have a new developer on your team. Or someone else wants to use that hot new library you just wrote. Either way, you need to find some way to explain your code. Your unit tests are simple examples of using the code you’ve written and are a great place to start for those who’re getting to know your codebase. While you still may need to help out your new admirers, you’ve lowered the grade of the learning curve.
Benefit #4: Once-and-for-all bugfixes
OK, so a bug slipped through your unit tests. Or perhaps it wasn’t a bug, but “unexpected behavior”. Assuming it’s not bringing down your entire site this instant, don’t fix it yet. Instead, write a unit test that exposes the bug. Make sure that the unit test fails. Then, fix the bug and ensure that the unit test passes. This idea, borrowed from the process of Test-driven development, is awesome for a few reasons.
First, that specific bug isn’t going to happen again. You wrote a unit test that fails when that bug occurs and passes when it doesn’t. Donezo.
Second, writing a unit test to expose the bug has taught you exactly what triggers it, perhaps revealing a better solution to the underlying problem. For example, if the program chokes on the input “Gruß Gott!”, instead of simply disallowing unicode (which may bother your international users), you might instead find out the proper way to handle unicode. You’ll have a long-term fix and be done with the bug for good!
Benefit #5: Developing good internal APIs
One true downside of having a comprehensive test suite is that a new parameter to the critical path can have you updating a lot of your tests. This is, indeed, a huge pain. But once you have to do it the first time, you’re much more careful about having well-designed internal interactions among the components of your application. Ideally, your application is decomposed to the point where rewriting the interface to a particular component doesn’t have a cascading effect on the codebase. Investing in writing thorough unit tests for each component ensures that you’ll design it to minimize changes to its interface.
Benefit #6: Comprehensive regression tests
Sometimes, you have to make that sweeping change to rearrange the components of your application. Perhaps your codebase represents many developer-months of work, and you’re not sure if you just re-introduced all of those weird corner cases you’ve fixed and forgotten. If you’ve been diligent about covering every nook and cranny of your application with unit tests, you can be reasonably confident that when those unit tests pass, your application is as correct as it was before the change.
That doesn’t just go for large changes. Every incremental change can affect other pieces of the application, no matter how well-decomposed it is. Particularly when working with a team, having a complete regression test to run with every new feature is a great comfort. Even better, if the entire suite is run automatically before allowing every code checkin, you can rest assured that whatever’s in the repository is clean and ready for release.
Don’t get too cocky. Your unit tests are only as good as their author. If your tests aren’t written correctly or don’t cover all possible execution paths, you can still have bugs in your code. Unit tests are not a replacement for end-to-end tests or your QA team. Weird things can happen when all the pieces of your application are running together with production data, and you can’t write a unit test for everything. But you, and the long-term development of your application, much better off for it!