A Deep Dive
How To Build a Minimal Unit Testing Framework in C++
5 macros are all you need to build a no-bloat unit testing framework that gets the job done. Read more to learn how you can isolate test cases and prevent your program from crashing.

Creating a Test framework isn’t just for the big players. In fact, the underlying software principles are so simple that you can build your own framework with just a little effort.

Frameworks like GTest, Catch, and others use macros to hide all the magic. This way, you can focus on writing tests without having to deal all the boilerplate code.

With a bit of OOP, C++ templates, a design pattern and just five macros, we can build a unit testing framework that is minimal yet robust against segfaults or unexpected aborts.


Our framework needs to do five basic things in order to successfully run tests, namely: declare, define, register, run, and compare. And we’ll need five macros to do the same.

Let’s define them one by one.

Declare And Define Tests

Macros

The DeclareTest macro creates a singleton that inherits methods from its parent, the UnitTest (more on this later).

The DefineTest macro allows us to define the actual test that is run when we lauch the program.

The ## operator, also known as the token pasting operator, allows us to create a unique class name for each testcase.

To generate a unique class name, either the Module or the TestName argument in the DeclareTest macro must be unique.

Here’s how you’d use it in your project.

The UnitTest Class

The UnitTest class is also a singleton and a central part of the framework. Its primary objective is to manage and run all tests that are registered with it.

With a protected constructor, test cases can construct their own copies of the UnitTest class, and access or inherit its members. But, there is only one instance of the class that is publicly available. And, we access it via the getInstance() method.

The static testList is used to store pointers to test cases that are registered with the singleton. This way, we can access the runFunc() method overriden in test definitions.

The expectEQ() method compares the actual value returned from the test to its expected value. And finally, the runTests() method runs all registered test cases in a for loop.

Register, Run, and Evaluate Tests

Now, all that’s remaining is to register, run, and evaluate test cases.

We’ll do this with three simple macros: RegisterTest, RunTests, and the ExpectEQ macro to make comparisions easy.

Putting It Together: A Sample Test Program

Let’s write a demo test program to see how it all fits together.

Like any other program in C/C++, you have a header file to declare, a source file to define, and another source file with a main() function to generate an executable. Simple. Right?


🔖  How To Stop Tests From Crashing Your Program

Running tests in separate processes saves your program from crashing when one of them throws an exception or aborts. Since each test case runs in its own isolated process, the damage doesn’t spread.

The code below converts the runTests() method into a multi-process function with vfork() (UNIX) system calls.

In lines 5 and 6, we have defined passed and failed as static variables. This is due to the fact that child processes have their own copies of these variables. Incrementing them in each child process does not reflect the total number of tests unless they are defined static.

Now, you can debate whether to use fork() or vfork() to create child processes. I chose vfork() due to its blocking nature, i.e., the main process waits until its child process returns, before executing the next instruction. This prints console logs in the right order.


This is as simple as it can get. A functional yet minimal unit testing framework in C++.

Checkout the complete project on GitHub!