Tiny C++ Unit Test Framework
I was working on a small project for personal use that I wrote in C++ and I wanted to test it. Since it’s a tiny project, I didn’t bother installing a well-known test framework that ships with a plethora of features. I just wanted a simple test runner that can show me a failure when it happens. Basically, a console application that runs the tests and displays a report at the end of the run. I wanted something similar to the unit test framework that ships with Visual Studio for managed code, in other words, I don’t want to add a call in main() to each new test method I add. Because if I forget to call a test method in main(), it will be in vain.
I came up with a macro that satisfied my requirements, and made my test cases look like the following:
1: //----------------------------------------------------------------------------
2: //
3: // Unit tests.
4: //
5: //----------------------------------------------------------------------------
6:
7: #include "UnitTest.h"
8:
9: TEST_METHOD(RootNotNullTest)
10: {
11: Tree tree(1);
12:
13: assert(tree.GetRoot() != NULL);
14: }
15:
16: TEST_METHOD(SumTest)
17: {
18: Tree tree(5);
19:
20: assert_m(5 == tree.Sum(), "Sum should be 5");
21: }
All what I needed to do was to create the test methods using my TEST_METHOD macro. In the first method, I used the assert macro defined in <assert.h>. If that test fails, the error message will look like the following:
Assertion failed: tree.GetRoot() != NULL, file d:\testrunner\unittest.cpp, line 13
Most of the time, one needs a more explanatory message, that’s why I defined my own assert macro: assert_m, which takes a massage and displays it:
Assertion failed: Sum should be 5, file d:\testrunner\unittest.cpp, line 20
As you can see, main() is not in the test cases file, it’s actually in UnitTest.h, so that I can reuse that header file in other projects. All what I need is to include it and voila, it works!
The TEST_METHOD macro defines a prototype for the test method, then adds the test method name and a pointer to the function in a global vector. I also added a method called Run() that loops through the vector to run the tests and print out the results. However, once an assert fails, the test run is aborted, so you only get one failure at a time, which is not a big deal since the pass rate should be 100%.
Here’s the code of UnitTest.h:
1: //----------------------------------------------------------------------------
2: //
3: // Unit test runner.
4: //
5: //----------------------------------------------------------------------------
6:
7: #include <assert.h>
8: #include <vector>
9:
10: #define TEST_METHOD(_Method) \
11: /* Function prototype */ void _Method(); \
12: /* Register as a test */ RegisteredTest g_##_Method (TestCase(#_Method, &_Method)); \
13: /* Function declaration */ void _Method()
14:
15: #define assert_m(_Expression, _Message) \
16: (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(_Message), _CRT_WIDE(__FILE__), __LINE__), 0) )
17:
18: typedef void(*PTestFunc)(); // Test function pointer type
19:
20: struct TestCase
21: {
22: TestCase(const char* Name, PTestFunc Method)
23: : m_Name(Name), m_Method(Method)
24: {
25: }
26:
27: const char* m_Name;
28: PTestFunc m_Method;
29: };
30:
31: std::vector<TestCase> g_TestCases;
32:
33: struct RegisteredTest
34: {
35: RegisteredTest(const TestCase& test)
36: {
37: g_TestCases.push_back(test);
38: }
39: };
40:
41: void Run() // Runs tests and displays report
42: {
43: int i = 0;
44:
45: printf("# %-30s\tResult\n", "Test");
46: printf("---------------------------------------------------------------------------------\n");
47:
48: for (std::vector<TestCase>::iterator t = g_TestCases.begin(); t != g_TestCases.end(); ++t)
49: {
50: printf("%-5i %-30s\t", ++i, t->m_Name);
51: t->m_Method();
52: printf("%c Passed\n", 258); // 258 = Smiley face
53: }
54:
55: printf("---------------------------------------------------------------------------------\n");
56: }
57:
58: int main(int argc, char* argv[])
59: {
60: Run();
61:
62: return 0;
63: }
Here is a screenshot of a run failure:
And one that passed: