In order for our unit tests to become effective we need to lay out some ground rules. What is a good unit test and what can we do to make sure we create tests that actually help us in our development efforts?
Because it’s not always easy or possible to create the perfect solution from the first time I have categorized unit test in three different categories:
1. A basic unit test:
- does not depend on the environment; e.g. it will run on your computer and it will run on your colleague’s computer
- does not depend on other unit tests
- does not depend on external data
- does not have side effects
2. A good unit test:
- asserts the results of your code
- tests a single unit of work (mostly a method)
- Covers all the paths of the code you want to test
3. A better unit test:
- tests and asserts edge cases and different ranges of data
- runs fast (< 100ms)
- is well-factored
Let’s see what the requirements for a basic unit test are and what this implicates:
1. Does not depend on the environment
In order to be able to run your unit tests frequently and on every check-in to your SCM, you need to make sure that it doesn’t require a specific environment. This means that they can’t rely on certain settings on your machine, files being present and any other thing that is related to your computer.
This implicates that your code cannot depend on it either. But what do I do when my program reads files from the disk? In a next post I will discuss some techniques to decouple code from the actual file system and other dependencies. This is what it is meant by “making your code testable”.
2. Does not depend on other unit tests
To make sure that one failing unit test doesn’t affect other unit tests, you have to make sure they can be run independently. The power of unit tests is that they will clearly indicate the source of the error and even tell you how to fix it. If your first test fails and all other tests depend on it, you won’t have this advantage because all of a sudden all tests will fail and you won’t know where it went wrong.
3. Does not depend on external data
For the same reason as it cannot depend on the environment you should make sure that no set up of external data is required. That means for example that accessing your database is out of the question. Instead your unit test should provide the data to your code. That way the test is portable and you have more control over the input.
4. Does not have side effects
A unit test shouldn’t cause external effects. For example you don’t want a test to be sending out e-mails or updating records in a database. If you provoke side effects, you need to manage your tests a lot more because you don’t want 300 mails to be sent just because you were running tests all day long. Apart from that, the side effects are not testable.
For a good unit test we need a few more things to be in place:
5. Assert the results of your code
This one is kind of obvious but I mention it because it is very important to create good assertions. An assertion should make clear the supposed behavior of your code. Try to assert all output from your code.
6. Test a single unit of work
It’s important to test small parts independently. If you test a big chunk of code at once, you will run into a maintenance nightmare. Testing big pieces of code will cause a lot of changes in your unit tests whenever changes in the system occur. If you test small pieces, only one or a couple of tests will be affected by a change in the system: the tests that actually test the code has changed. Apart from that, the narrower the scope of a single test is, the clearer the solution to the problem will be when the test actually fails.
7. Cover all the paths of the code you want to test
If you want a good unit test, you at least have to make sure that all your code is executed at least once, including that strange exception handling condition you have somewhere.
And to top it off, with these extra’s you have a very good unit test:
8. Test and assert edge cases
Testing all your code on all paths I usually not enough. Under normal circumstances you need to test the same path with different data. The more edge cases and different data structures you include in your tests, the more reliable they will be.
9. Run fast
The time limit I put in the introduction is a little bit arbitrary, but it should be considered a maximum. Why? The idea of unit testing is that you build up a suite. Once you have a suite of tests, you run it as often as possible. Let’s consider we have about 100 tests (which is a fairly low number on a medium-sized project considering the fact that we try to test the smallest possible units). With a 100 tests our test-run will take 10 seconds. That’s already a a fair amount of time to be executing very frequently. Now change that value to a 1000 and it will already take about a minute and a half. To do that every time you build your project will be annoying. The problem is that you’ll not let it get to that. You’ll either improve the speed of the unit test, or you will stop executing them.
In the end, unit tests are code. Every code we write will be read a lot by other developers and yourself. Therefor, treat your unit tests as if they were production code. The better you set up your unit testing structure the easier and more flexible (and enjoyable) it will be to work with.
In this post I summed up what I think are the most important characteristics of a unit test and in what order. When you’re just starting to write unit tests, the goal shouldn’t be to get them all a 100% right the first time, but try to reach for at least cases 1 to 7. After that you can start improving your tests by implementing the others.
In further posts I will address how you can achieve this.