Unit-test in C++: what should you know.
Nikolai Kutiavin
Professional problem solver in C++ and beyond | C++ design and architecture expert
Unit tests are important for a single reason - they prove that a single component works as expected in isolation. If a test fails, it's usually easy and fast to find the cause. Unlike integration tests, unit tests are much faster and can be run after every commit. Just compare running a database, updating it, and dealing with cross-process communication to executing a single binary.
The Difference Between Testable and Untestable Code
There are many discussions about how useful and important unit tests are. For me, it was hard to start using them initially because you rarely have pure functions without any external dependencies.
Let's compare two functions below. The first just sums two numbers:
The second reads those numbers from a file and then sums them:
The first function is easy to test, but the second one is much harder because it depends on the filesystem.
Bringing Testability to the Code
Following Andrew Koenig's fundamental theorem of software engineering:
We can solve any problem by introducing an extra level of indirection.
To make your code testable, add another level of indirection. Instead of using a final class in readAndSum, use an interface like std::istream to decouple the code from the underlying filesystem. In this case, you can use std::istringstream with predefined content in the unit test.
Now let's consider a more complex example with the class FolderManager, which is responsible for creating files in a folder while maintaining a specified number of files. Here's the API:
With this simple implementation, the class is tightly coupled with the filesystem, making unit testing impossible. It may require filesystem functions like:
By moving these file operations into a separate interface, testability is easily achieved:
Then inject this interface into the FolderManager class:
In production, you can use the real filesystem functions, but in unit tests, you can replace them with mocks.
Another way to achieve testability is by using a template parameter:
领英推荐
So, you have options to choose from.
Tips for Testing
The main advice: Use existing libraries for mocks and tests—don't reinvent the wheel.
It might seem easy to create a mock as a child class inherited from the interface and hardcode all expectations, but it's much easier to learn a mocking framework like GMock or others and use them. For unit tests, you can try GTest or Catch2. The main benefits:
Remember—tests should be simple and crystal clear. Forget about optimization and complexity here. If a bug appears in your test, it'll be hard to debug.
For example, using GMock isn't difficult. Related to our previous example, a mock might look like this:
You can then write a simple and self-expressive test:
If you need more flexibility, such frameworks offer many customization options, like user-defined matchers in GTest.
For complex initialization across multiple tests, you can use an initialization section. GTest has a fixture setup in the constructor of the test class, as in the example below:
Then, in the test case, the setup code is shorter:
Summary
In summary, unit testing is not the hardest thing to master. To start:
Only modern C++ and Python | Open to project Collaborations|
4 周Yeah I absolutely agree. Premature optimization must be avoided in the very beginning. The function or the feature must be tested on the implementation that is as simple as possible yet related to the actual use-case.
Senior Software Developer presso MAGNA
1 个月I agree ??, providing common interfaces to allow mocks implementations for external dependencies, and using fixture classes to avoid redundant pieces of code in many test bodies (or templates approach), is the right way to do these things. Unfortunately, the reality is sometimes different, it happened to see it, due to rush, pressure and wrong priority to deliver something anyway and combined with a lack of responsibility when design... that after months or even years of work, the result is one where the code turns into a mixture of unwanted tricks as ifdef’s TEST_ON under where are declared/defining test methods versions (different includes) or even changing private to public using the same ifdef approach, or the introduction in the header of some friend test functions declarations to the class to allow the access to the private methods or variables of the tested class… and many other “patch” to hide a wrong design ?? The best practice and approach should be to have this in mind before to start coding, and align the delivery and the responsability of a quality of code with the reality of doing the things in the right way, not only in the fast way ??
Senior R&D Specialist at Efacec
1 个月I have used gTest before but not catch2 . Is mocking easier at catch2 ?
Computer Science Engineer
1 个月it is simple and crystal clear, thanks for sharing.
SWE Fellow @HeadstarterAI | xNeuroLeapCorp, Technical Writer @OpenGenus, Content Advisor @LogRocket, Computer Engineering.
1 个月. I almost fell for the trap of manual mocking of the interface in my head while reading this but as it turns out, using a library could help. Havent done much testing, will try Gmock. Very informative read btw, thanks!