Write Your Own C++ Unit Testing Library
By abusing the fact that you can run code before main()
by putting it in the
constructor of a global variable, you can make a very small, yet serviceable,
unit testing library in C++:
#include "test.h"
TEST(OnePlusOne) { EXPECT(1 + 1 == 2); }
TEST(OnePlusOneFail) { EXPECT(1 + 1 == 3); }
int main() {
int err_count = run_all_tests();
return err_count == 0 ? 0 : -1;
}
The output:
[ PASS ] OnePlusOne
[ FAIL ] OnePlusOneFail: 1 + 1 == 3
1 out of 2 tests passed
Behind The Scenes
test.h
takes each TEST()
and stores them in a vector of TestCase
structs.
struct TestCase {
const char *name;
void (*run)(const char *&);
};
static std::vector<TestCase> global_all_tests;
Let's take TEST(OnePlusOne) { EXPECT(1 + 1 == 2); }
as an example: The name
for this test case will be set to "OnePlusOne"
, and run
will become a
pointer to a function that executes EXPECT(1 + 1 == 2)
when invoked.
This test case gets added to global_all_tests
and the run_all_tests()
function
goes through each test case in the vector and calls the run
function on each of them.
Here is the implementation of the TEST()
and EXCEPT()
macros:
#define TEST(name) \
void(_test_##name)(const char *&); \
static void _init_##name() { \
global_all_tests.push_back({#name, _test_##name}); \
} \
struct _struct_##name { \
_struct_##name() { _init_##name(); } \
}; \
static _struct_##name _var_##name; \
void(_test_##name)(const char *&_test_reason)
#define EXPECT(cond) \
if (!(cond)) { \
_test_reason = #cond; \
return; \
}
To break this down:
void(_test_##name)(const char *&);
is the function signature for the test case. The implementation is for the user to fill in, which is why theTEST()
macro ends with the same function, except the body is missing.static void _init_##name() { ... }
is a function that will get called beforemain()
. The function_test_##name
is added toglobal_all_tests
.struct _struct_##name { ... };
is a type that calls the function above in its constructor.static _struct_##name _var_##name;
actually runs the_init_##name()
function by creating a variable of the type that was written above.void(_test_##name)(const char *&_test_reason)
is the actual function for the test. To check if a test passed, each test function takes a string. The value of the string passed in will be null, and theEXCEPT()
macro sets the string whenever the test fails. The test passes when the string remains null.
That's pretty much it. Create a run_all_tests()
and create const char *
variable.
Make a loop and pass the variable to each test case. For each test, check if the
variable gets mutated to see if it passed or failed.
The Full Source Code
Here is test.h
in its entirety. It's only 47 lines.
#pragma once
#include <cstdio>
#include <vector>
struct TestCase {
const char *name;
void (*run)(const char *&);
};
static std::vector<TestCase> global_all_tests;
#define TEST(name) \
void(_test_##name)(const char *&); \
static void _init_##name() { \
global_all_tests.push_back({#name, _test_##name}); \
} \
struct _struct_##name { \
_struct_##name() { _init_##name(); } \
}; \
static _struct_##name _var_##name; \
void(_test_##name)(const char *&_test_reason)
#define EXPECT(cond) \
if (!(cond)) { \
_test_reason = #cond; \
return; \
}
static int run_all_tests() {
const char *reason = nullptr;
int passed = 0;
for (const auto test : global_all_tests) {
test.run(reason);
if (reason) {
printf("[ FAIL ] %s: %s\n", test.name, reason);
reason = nullptr;
} else {
printf("[ PASS ] %s\n", test.name);
passed++;
}
}
printf("\n%d out of %u tests passed\n", passed, global_all_tests.size());
return global_all_tests.size() - passed;
}