Unit-test in C++: what should you know.

Unit-test in C++: what should you know.

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:

  • Getting files in a folder
  • Checking if a folder exists
  • Creating a file
  • Removing a file

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:

  • A generic API—if a function is unfamiliar, you can just google it.
  • A simple and reliable way to test expectations.
  • Utilities like parameterized tests, where you write the code once and run it with multiple input samples.

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:

  • Make your code testable.
  • Use frameworks for unit tests and mocking—avoid reinventing solutions.
  • Use all the capabilities of the chosen framework to create simple, understandable tests.



Ayush Kaintura

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.

回复
Romeo Cureliuc

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 ??

Hassin A.

Senior R&D Specialist at Efacec

1 个月

I have used gTest before but not catch2 . Is mocking easier at catch2 ?

Burak Or?un ?zkablan

Computer Science Engineer

1 个月

it is simple and crystal clear, thanks for sharing.

Hamza Mateen

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!

要查看或添加评论,请登录

社区洞察

其他会员也浏览了