Unit tests and micro tests

There are quite some ways to get to a level of quality of a piece of source code: static code analysis, coding standards or a review process to name a few. But there is only one way to ensure that the software behaves as intended, and that is through testing. Since it is very tedious to perform the same tests over and over again when a piece of code changes, it pays to automate tests where possible. In order to have a good test coverage – in the sense that a great part of the defined behavior is tested – unit tests are indispensable. Unit tests allow to secure the behavior of the smallest parts of the code.

But what exactly are unit tests? It is tempting think that a unit test would test the basic unit in the programming language. In case of an object oriented language, a unit test would test the behavior of a single class. A test would contain the test cases that together validate the class under test’s behavior. A class car would test that its wheel members start spinning when giving the engine full throttle, that the front wheels turn when turning the steering wheel, and so on. But it would not test that the engine stops when it runs out of fuel, because that would belong to the engine tests. It would look like this, where each class has an associated test class counterpart:

https://mermaid.ink/img/pako:eNplj8EKgzAMhl-l5KwvUHYZ257AXQa5BBs3waajTQ8ivrsdOJXtlnz58pNM0AbHYKEdKKVrT89IHgWlCZ4vH2ZOdW227s5JE8pZgr447sIRrM6D9U_7YVvaeEwa9xlU4Dl66l05cUIxBqFse0awpXTcUR4UAWUuan47Ur65XkME29GQuALKGppRWrAaM3-l9dPVmheOK122
Each production class has an associated test class

Another approach

That’s at least what I used to think for a long time. However, this definition of unit is not necessarily true. It is also possible to see a unit as a module, a set of coherent classes that together act as a higher level unit. Then, in order to secure the behavior of the module, the unit tests validate that the public interface of the module works as intended.

Take for example some library that you might be working on. Most libraries contain at least a handful classes. There are a select number of classes that constitute the public module interface, but those classes may use other classes themselves. Those other classes may not even be visible to the outside world. It can be beneficial to write the unit tests against the public module interface. This results in something like this, where the test only interacts with the public interface of the module:

https://mermaid.ink/img/pako:eNp9kLsOwjAMRX8l8twujBlYeEgMTDB6sRIXKiUOymNAVf-dUIpUkGCz7zm6sjyACZZBg3GU0ranSySPgnIMtjjekyHLZ045qbZdq2X66Uz4IMJx82z6C1e_6CtFWSzfcCqABjxHT72tlw8oSiHkK3tG0HW03FFxGQFlrGq5Wcq8s30OEXRHLnEDVHI43cWAzrHwW5ofMFvjA3lsZIs
The unit test only uses the public interface of the module

Differences between test types

In order to better distinguish between the two types of tests, the first type is often referred to as micro tests. I’ll do that in this article as well, to keep the text understandable. The two test types have some profound differences.

Micro tests are the most low level of the two. Micro tests can be useful to test a small part of complex functionality; for instance when a method has a complex calculation, it is useful to validate it using micro tests. There may be a single test class for just that one complex calculation. Large scale micro testing comes at a price though: operational and test classes are tightly coupled because they have the same structure. It makes refactoring difficult: you cannot easily move functionality from one class to another because then you also must move the associated tests. Even more difficult is extracting a class from another class: this requires at worst a complete rewrite of all tests because previous assumptions on the class no longer hold.

So unit tests might make life better in this regard, and, in my experience, it actually does. But there is a price to pay: since the test area is larger (more code under test), the test setup will be much more complex. I found the test data builder pattern helps to mitigate this. When done well, each test case has at most a handful lines of code. But there are two drawbacks: it requires more up front work to create the test data builder itself, and the additional abstraction layer makes it sometimes more difficult to grasp what is going on, especially when a test fails. Still, I see a lot of value in this type of test because it makes it possible to deliver validated code and allows refactoring at the same time.

Leave a Reply

Your email address will not be published. Required fields are marked *