Many developers talk about unit tests, but it is not always clear what they mean. Sometimes there seems to be no distinction to other kinds of tests, and often there are discussions about what unit tests should be used for. In this article, we will talk about the purposes of unit testing so let’s start right in!
Proving functional stability
Automated tests are there to give us a sense of security that our software works as intended. The automation makes sure that the same tests can be executed again and again, so passing unit tests can show a developer that changes he applied to the software did not break anything he did not want to break.
This also means that if a developer makes a breaking change to the code, he should be able to observe that change in failing tests. Anticipating those test failures and comparing our expectations with the test results give us a hint where we might have misunderstood either the existing code or the effects of our changes.
Distinction to other tests
All that was written in the above section applies to any kind of automated test. I did not even use the words “unit test” in there. So what is the difference between unit tests and other automated tests?
The answer is in the name: The word unit means that we don’t test the whole system or a bigger part of the system. It means we perform tests at a fine granularity.
That is the primary distinction from unit tests to system tests, where whole systems or subsystems are tested and to integration tests, where the interaction between a group of classes is tested.
A major benefit of the fine granularity of unit tests is that once a test fails, the reason for that failure resides in a small area of code, which means it is easy to find and often equally easy to fix.
What is a unit, anyways?
Often, people automatically think of class tests when they say unit test. However, it is not always that easy, especially not in C++ where not everything is a class.
A unit can be defined as a small, cohesive piece of code. In a clean design that adheres to the usual design principles of software, that often means it is indeed a single class. But it also could be a set of functions, or a small set of classes if a single class can not provide a self-contained set of functionality.
A unit is a small, cohesive, self-contained set of functionality, which is often, but not always, a single class.
This does not mean that a unit contains any dependencies a class or function has to rely on. If a unit has hard coded dependencies on a greater set of classes, a test failure could originate in any of those classes and the fine granularity that makes a test a unit test is lost.
Loose coupling is essential to enable unit tests.
Other uses of unit tests
Besides from proving the functional stability of a unit of code, unit tests can serve in other ways, too.
Documenting the code
Unit tests can go a long way towards documenting the usage of the code in question. A thorough set of tests that cover the use cases, boundaries and error conditions are as good as any made-up example code, with the additional benefit that they get compiled and run and proven to be actually correct examples.
If the unit is easy to use (which it should be) I would even say that such a unit test suite is sufficient and no further documentation, e.g. in the form of doxygen comments is needed.
Test driven development
With TDD you first write unit tests that test the functionality you would like to have. At first those tests will fail or even won’t compile, so you write the code that is needed to pass the tests.
It is a key philosophy of TDD that you write only the code needed to pass the tests, so that there will be no unnecessary code. When all tests pass, you refactor your code to clean it up and start the cycle again writing the next test.
There is much more to be written about TDD, but the key take-away for this post is that there is no TDD without unit tests, and that it therefore makes it hard to write heavily entangled classes.
Since in TDD there is no code that is not under test, that means that unit tests in TDD automatically document all functionality that has been implemented.
Getting to know the code
When diving into an undocumented, sparsely tested and hard to grasp piece of legacy code, you should consider writing unit tests for it. Writing unit tests for legacy code may be tedious but can prove valuable in different ways:
- It gives assurance that you correctly understood what the code does
- It leaves a piece of documentation for others who might get into the same situation
- If you plan to refactor or otherwise change the code, you definitely should bring it under test first, because unit tests are your safety net for such actions