Guidelines to writing effective unit tests
It's one thing knowing why you should focus on your unit testing strategy and what ot unit tests, but none of that is helpful if you don't write effective unit tests. . For the sake of both developers and testers, these guidelines will help you ensure that your unit tests are written effectively, and in a manner that testers can contribute to easily.
To determine whether a certain scenario would be suited for unit testing, use the following characteristics to help you:
Fast: It is not uncommon for mature projects to have thousands of unit tests. Unit tests should take very little time to run - milliseconds even.
Isolated: Unit tests are standalone, can be run in isolation, and have no dependencies on any outside factors such as a file system or database.
Repeatable: A unit test’s results should be consistent; this means that it should always return the same result if nothing is changed between runs.
Self-checking: The test should be able to automatically detect if it passed or failed without any human interaction.
Timely: A unit test should not take a disproportionately long time to write compared to the code being tested. If you find that testing the code is taking a large amount of time compared to writing the code, consider a design that is more testable.
If your specific scenario does not suit any of these, it’s likely that it would be best tested in another way.
Best practices for unit testing
The following guidelines should assist teams in writing effective unit tests that will also appeal to the needs of the testing team.
1. Naming your tests
Tests are useful for more than just making sure that your code works, they also provide documentation. Just by looking at the suite of unit tests, you should be able to infer the behaviour of your code. Additionally, when tests fail, you can see exactly which scenarios did not meet your expectations. The name of your test should consist of three parts:
The name of the method being tested
The scenario under which it’s being tested
The behaviour expected when the scenario is invoked
By using these naming conventions, you ensure that its easy to identify what any test or code is supposed to do while also speeding up your ability to debug your code.
2. Arranging your tests
Readability is one of the most important aspects of writing a test. While it may be possible to combine some steps and reduce the size of your test, the primary goal is to make the test as readable as possible. A common pattern when unit testing is “Arrange, Act, Assert”. As the name implies, it consists of three main actions:
Arrange your objects, by creating and setting them up in a way that readies your code for the intended test
Act on an object
Assert that something is as expected
By clearly separating each of these actions within the test, you highlight:
The dependencies required to call your code,
How your code is being called, and
What you are trying to assert.
3. Write minimally passing tests
Tests that include more information than is required to pass the test have a higher chance of introducing errors, and can make the intent of the test less clear. For example, setting extra properties on models or using non-zero values when they are not required, only detracts from what you are trying to prove.
When writing unit tests, you want to focus on the behaviour. To do this, the input that you use should be as simple as possible.
4. Avoid logic in tests
When you introduce logic into your test suite, the chance of introducing a bug through human error or false results increases dramatically. The last place that you want to find a bug is within your test suite because you should have a high level of confidence that your tests work. Otherwise, you will not trust them and they do not provide any value.
When writing your unit tests, avoid manual string concatenation and logical conditions such as if, while, for, or switch, because this will help you avoid unnecessary logic.
5. Prefer helper methods to Setup and Teardown
In unit testing frameworks, a Setup function is called before each and every unit test within your test suite. Each test will generally have different requirements in order to get the test up and running. Unfortunately, Setup forces you to use the exact same requirements for each test. While some may see this as a useful tool, it generally ends up leading to bloated and hard to read tests. If you require a similar object or state for your tests, rather use an existing helper method than leverage Setup and Teardown attributes.
This will help by introducing:
Less confusion when reading the tests, since all of the code is visible from within each test.
Less chance of setting up too much or too little for the given test.
Less chance of sharing state between tests which would otherwise create unwanted dependencies between them.
6. Avoid multiple asserts
When introducing multiple asserts into a test case, it is not guaranteed that all of them will be executed. This is because the test will likely fail at the end of an earlier assertion, leaving the rest of the tests unexecuted. Once an assertion fails in a unit test, the proceeding tests are automatically considered to be failing, even if they are not. The result of this is then that the location of the failure is unclear, which also wastes debugging time.
When writing your tests, try to only include one assert per test. This helps to ensure that it is easy to pinpoint exactly what failed and why.
Like with all things coding related, knowing the theory is not enough and it requires practice to get good and build a habit, so these unit testing practices will take time to get right and feel neutral. The skill of writing a proper unit test though is incredibly undervalued and one that will add a lot of value to the quality of the code so the effort and extra effort required is certainly worth it.