Unit tests are great. I use them in my projects. However, over the years I've had an ongoing internal dialogue about their purpose.
I have often fallen into the trap of over testing my code. Projects that should have shipped in a few short weeks have been mired in a mess of me simply trying to get the current tests to pass.
I've also worked on projects that would have been more successful if there were more tests. I've shipped too early.
This got me wondering how much unit testing is too much? At a fundamental level, what core problem do unit tests solve?
When you boil it down, unit tests are just another simulation tool. They give you the power & freedom to trigger specific conditions in which your application is expected to behave a certain way.
Let’s break that down a little more clearly.
Most consumer apps can be understood as a responder of some sort. You tap a button, and the software responds by taking you to another screen or doing some action. The condition is the tapping of the button, and the response is the navigation to another screen.
- Simple and easy to trigger.
- More complicated and difficult to trigger.
- Simple but requires time to trigger, date-based phenomena.
A simple and easy condition is the pressing of a button. To trigger that condition, all you need to do is launch your app and go to the screen with that button. Then you need to push the button.
It’s really easy to test what happens when you trigger this condition. It takes as much time as it takes to run your app and go to the screen with the button. You can do it in less than a minute.
But what would a more complicated condition look like? The state of checkmate in the game of chess can serve as an example.
Checkmate happens when a player’s king is unable to move to another tile on the board without being threatened by the opposing player’s pieces.
If you were to build a game of chess, you would definitely need some function to detect if a player is in checkmate. Otherwise, you would have no way of knowing when the game is finished or who won.
You would also need to know with 100% certainty that your checkmate detection function covers all possible scenarios (a.k.a. edge cases). In essence, you would need to run a large number of tests.
The quick and obvious thing most people will think to do is simply play a bunch of games of chess to see if the checkmate detection function works as intended. If it fails to detect checkmate or incorrectly detects checkmate when no state of checkmate has been reached, the function still needs some work.
However, if after playing through countless games of chess, you discover that your checkmate detection function always correctly attributes checkmate when it should and never when it shouldn’t, you can be reasonably certain that your checkmate detection function works as intended.
But here’s the rub. If you were to do that, you would need to play through a ridiculous number of games of chess to reach a certain level of confidence in your checkmate detection function. You would also need to wait an additional amount of time for random chance to create the various edge case scenarios you want to test.
That’s because checkmate can only happen after a complex set of conditions that are difficult to trigger manually. You have to play an entire game of chess to get there.
I don’t know about you, but I certainly don’t have an entire lifetime to spend waiting to know if my checkmate detection function works the way I want it to. With that in mind, wouldn’t it be great if there were a way to test your checkmate detection function without playing through an absurd number of games?
It would be amazingly helpful if you could trigger some of those edge case scenarios on your own instead of waiting for random chance to do it for you. Indeed, that is what unit tests are for, and that is the problem they solve.
Unit tests are a simulation tool. They help you setup the various conditions in which your app is expected to work a certain way.
With unit tests, you can simulate any number of fake games of chess to verify that your checkmate detection function works as expected. You don’t have to play through a bazillion games of chess anymore! You can simply jump to the end of the game by setting up a fake board with pieces in the positions you want to test.
Once you setup enough fake games to properly cover all of the possible scenarios for your checkmate detection function, you will be able to determine with 100% certainty that your checkmate detection function does what you expect it do (and also doesn’t do what you don’t expect it to do).
Each of those fake games is what we would call a unit test case. Put enough of them in the same place, and you have a unit test suite.
That’s really all unit testing is. You come up with a bunch of fake scenarios to cover all of the cases, and then you sit back and watch how certain functions in your app respond to them.
Unit tests solve a very basic and not-so-obvious problem in software development. It is a problem that arises when certain functions in your app are buried under complex or time-consuming triggering conditions.
You have a text box that is supposed to say “Happy Wednesday!” next Wednesday, and you want to test it. But you don’t want to wait until next Wednesday to see if it contains the correct message. You want to test it right now. Unit tests give you a way to do that.
Of course, unit tests alone can’t help you. Unit tests only work when you structure your code in a way that makes it easy for unit tests to do their job. That is why the practice of unit testing will make you a better developer overall. It forces you to build discrete modules with a well-defined purpose and interface.
With unit tests, you can unearth some of those difficult-to-trigger functions, saving vast quantities of time that would have otherwise been spent trying to setup the conditions in which your app is expected to behave a certain way.
There are those who will tell you that more unit tests is always a good thing. They advocate a practice known as test driven development.
Although I'm a huge fan of tests, I think we need to be a bit more pragmatic with their application. Do you really need to write a unit test for a simple condition that you can trigger in less than a minute by simply running your application and pressing a button?
If it takes significantly less time to write and run a unit test than it takes to manually test the same thing, unit tests can be a huge productivity booster.
That's easier in some environments than it is in others. To run unit tests in iOS, for example, you have to wait at least 30 seconds for the simulator to run. You wouldn't think the 30 seconds matters so much, but imagine having to do that for every atomic function you want to test. It adds up pretty quickly.
It also depends on the size of the project and the team you're working with. As the project and team scales, unit tests become more and more valuable because there's an increased chance that a piece of code you write will get modified later on.
With larger teams, it's more important to communicate the side effects a given button press or event has on the rest of the app. In that context, unit tests function as a kind of documentation. They help communicate the intent of the code you're testing.
It's important to understand that unit tests are a specific tool with a clearly defined purpose. They are great for getting at those hard to reach places, the difficult-to-trigger conditions that require a complex setup. If you use them where they provide the greatest amount of value, you'll save a great deal of time.
You're one click away
Level up every day