C++ Code Capsules
When Extreme Programming was all the rage in the late 1990s, automated unit testing was still a relatively fresh idea for many working software developers. JUnit and CppUnit emerged as the leading test frameworks for Java and C++, but I wanted something even simpler—code that students could understand and that was easy to use.
I thought I’d achieved it in the well-received article I wrote for the C/C++ Users journal in September 2000, The Simplest Automated Unit Test Framework That Could Possibly Work.
But I was wrong.
A few years later, while teaching and developing examples at Utah Valley University, I found myself wanting something even smaller. How small could I go? Did I really need inheritance? My freshman C++ students didn’t learn object-oriented programming right away anyway, but they needed to learn the importance of crafting robust code from the get-go, regardless of paradigm.
The result was a simple header file: test.h.
Individual tests need to be boolean expressions, of course, so
successes will be noted and failures should be flagged with their
location and the expression that failed. Code that generates exceptions
should also be validated. Capturing the source text of an expression
calls for the preprocessor’s stringizing operator (see
#cond below). (For a deep dive into the macro preprocessor
see
The
Preprocessor.) Here is the basic test function macro:
#define test_(cond) do_test(#cond, cond, __FILE__, __LINE__)If the source line is
test_(stk.top() == 1);then the preprocessor replaces it with the following text in the compilation stream:
do_test("stk.top() == 1", stk.top() == 1, "tstack.cpp", 17);indicating that the name of the file is tstack.cpp and
the call occurred on line 17 of that file. The function
do_test tests the condition and responds accordingly:
inline void do_test(const char* condText, bool cond, const char* fileName, long lineNumber) {
if (!cond)
do_fail(condText, fileName, lineNumber);
else
++nPass;
}
inline void do_fail(const char* text, const char* fileName, long lineNumber) {
std::cout << "FAILURE: " << text << " in file " << fileName
<< " on line " << lineNumber << std::endl;
++nFail;
}If the condition failed, the following would be reported:
FAILURE: stk.top() == 1 in file tstack.cpp on line 17
Code that expects an exception to be thrown under certain conditions can be verified by the following function macro:
#define throw_(expr,T) \
try { \
expr; \
std::cout << "THROW "; \
do_fail(#expr,__FILE__,__LINE__); \
} catch (const T&) { \
++nPass; \
} catch (...) { \
std::cout << "THROW "; \
do_fail(#expr,__FILE__,__LINE__); \
}The Stack class I am using for this example wraps
std::stack with member functions that throw
std::underflow_error when calling top or
pop on an empty stack (std::stack does not
throw). The following code tests proper exception handling:
Stack<int> stk;
test_(stk.size() == 0);
// Test exceptions (top and pop are invalid on empty stack)
throw_(stk.top(), std::underflow_error);
throw_(stk.pop(), std::underflow_error);
nothrow_(stk.size());For completeness there are also succeed_,
fail_, and nothrow_ macros. There is also a
report_ function. Here is the complete header file:
#ifndef TEST_H
#define TEST_H
#include <cstddef>
#include <iostream>
using std::size_t;
// Unit Test Scaffolding: Users call test_, fail_, succeed_, throw_, nothrow_, and report_
// AUTHOR: Chuck Allison (Creative Commons License, 2001 - 2017)
namespace {
size_t nPass{0};
size_t nFail{0};
inline void do_fail(const char* text, const char* fileName, long lineNumber) {
std::cout << "FAILURE: " << text << " in file " << fileName
<< " on line " << lineNumber << std::endl;
++nFail;
}
inline void do_test(const char* condText, bool cond, const char* fileName, long lineNumber) {
if (!cond)
do_fail(condText, fileName, lineNumber);
else
++nPass;
}
inline void succeed_() noexcept {
++nPass;
}
inline void report_() {
std::cout << "\nTest Report:\n\n";
std::cout << "\tNumber of Passes = " << nPass << std::endl;
std::cout << "\tNumber of Failures = " << nFail << std::endl;
}
}
#define test_(cond) do_test(#cond, cond, __FILE__, __LINE__)
#define fail_(expr) do_fail(expr, __FILE__, __LINE__)
#define throw_(expr,T) \
try { \
expr; \
std::cout << "THROW "; \
do_fail(#expr,__FILE__,__LINE__); \
} catch (const T&) { \
++nPass; \
} catch (...) { \
std::cout << "THROW "; \
do_fail(#expr,__FILE__,__LINE__); \
}
#define nothrow_(expr) \
try { \
expr; \
++nPass; \
} catch (...) { \
std::cout << "NOTHROW "; \
do_fail(#expr,__FILE__,__LINE__); \
}
#endifHere is the driver for the Stack class:
// tstack.cpp: Test driver for Stack<T>
#include "test.h"
#include "stack.h"
int main() {
Stack<int> stk;
test_(stk.size() == 0);
// Test exceptions (top and pop are invalid on empty stack)
throw_(stk.top(), std::underflow_error);
throw_(stk.pop(), std::underflow_error);
nothrow_(stk.size());
// Test push and top
stk.push(1);
test_(stk.top() == 1);
test_(stk.size() == 1);
stk.push(2);
test_(stk.top() == 2);
test_(stk.size() == 2);
// Test pop
stk.pop();
test_(stk.top() == 1);
test_(stk.size() == 1);
stk.pop();
test_(stk.size() == 0);
throw_(stk.top(), std::underflow_error);
throw_(stk.pop(), std::underflow_error);
report_();
}When executed the report_ function prints:
Test Report:
Number of Passes = 13
Number of Failures = 0
The code resides in the anonymous namespace, which is fine when only
testing code in a single file. That was sufficient for my purposes those
many years ago, but multifile projects will end up with separate
counters for success and failures. That is easily fixed by using a named
namespace with inline variables, but I wrote this
pre-C++17.
There is also the issue of code bloat due to including code in multiple files. That’s what modules are for.
In Part 2 I will fix these problems, but the need for stringizing remains to capture the text of the expression, calling for a hybrid preprocessor/module approach.