7 best practices for writing great software tests
An important metric of code quality is how much of your codebase is covered by tests, as we saw in a previous article about code coverage. However, having a good code coverage score isn’t enough.
The quality of your tests influences their effectiveness and can significantly affect the quality of your product. Today, we’ll cover 7 best practices for writing great software tests. Those practices help you get the most out of your testing strategy, so keep reading.
What makes a great software test?
- Simple and short: you should be able to understand and execute the tests;
- Goal-oriented: focusing on a single output gives clear purpose and meaning to the test;
- Consistent: all the tests should be consistent regarding the terms used;
- Valid: you should align the tests with the acceptance criteria defined for the requirements;
- Maintainable: tests should be easy to maintain and update when requirements change;
- Discoverable: you should be able to search and find a test using multiple criteria easily;
- Fast: you should be able to execute a test as quickly as possible to reduce effort;
Best practices for writing great tests
#1 – Treat your tests with the same importance as your code
Your tests require the same care and attention you give the rest of your codebase. Like any other part of your code, tests should be regularly evaluated, maintained, and improved.
You should also use comments in your tests and have a defined code review process. Tests take development time, so they should be planned, and you should design them to support and enhance your business’ critical infrastructure.
Finally, it would help if you kept your tests organized, as it allows avoiding repetitions. Get yourself and your team familiar with patterns for modularizing test cases.
#2 – Make your tests independent of each other
Tests should never depend on each other. For example, if your tests have to be run in a specific order because some tests depend on others, you need to change your tests to avoid those dependencies.
If, in your system, it’s unavoidable to have some tests dependent on others, try to isolate them as much as possible through pre-conditions. However, this is just in particular cases, and you should avoid it as much as possible.
#3 – Your tests should work individually or in parallel
It’s only natural that your test suite grows along with your codebase. When your test suite gets larger, writing tests you can run individually or in parallel on multiple machines really pays off.
This approach allows you to test only the parts of your code that you or your team changed. You can also better distribute testing resources across multiple machines, which helps you to get results faster. Finally, tests that can work individually or in parallel also mean that you can refactor them without fearing that change will affect other tests.
#4 – Refactor your tests regularly
As with any part of your codebase, you should regularly review and refactor your tests. As such, plan to survey your test suite from time to time to ensure your tests remain relevant and valuable.
When you change, add, or remove functions to your code, verify if the tests still make sense. Usually, with these changes in the code, the data passed into functions also changes, and your tests need to reflect this modification.
#5 – Minimize waits as much as possible
Tests that require “waiting” will likely cause more problems as services interact differently on various machines. In addition, if you run tests often, adding waits to a test suite can make it very slow.
If you’re testing asynchronous code, it sometimes appears to require wait or sleep steps. However, it would be best if you tried to incorporate a library designed to handle asynchronous waits in your programming language.
#6 – Test across the boundaries of your system
Your test suite should always test both sides of a given boundary. Imagine the piece of code you will test as a rectangle. You’ll need to test what happens to points inside and outside the rectangle and at the boundaries. These boundaries are where your code might fail.
If your code depends on external services, don’t forget to prevent problems by testing the boundary layers. This anticipation will allow you to have some control over code you might not have written and have no access to.
#7 – Keep track of your code coverage
Tools that keep track of code coverage, like Codacy Quality, can tell you exactly which parts of your code are covered by tests. This will help you direct your efforts toward pieces of code that lack tests.
To better enforce tests, you can also define thresholds where Pull Requests are not accepted if code coverage is below a specific percentage. Code coverage tools also allow you to track coverage over time, so you can report on and measure the results of your efforts.