test-part2
Automated Unit<br>Testing On-The-Cheap: Part 2
C++ Code Capsules
In Part<br>1 of this two-part series I introduced a time-tested (i.e., old :-))<br>technique that handled automated unit testing in a remarkably simple<br>way, including validating proper exception handling. The previous post<br>left two problems on the table, however, and the journey to fix them<br>turns out to be a nice tour of two key features of modern C++:<br>inline variables and modules.
The simplicity of the test framework discussed in Part 1 follows from<br>everything being contained in a small header file, test.h<br>(include guards not shown):
namespace {<br>std::size_t nPass = 0;<br>std::size_t nFail = 0;<br>inline void do_fail(const char* text, const char* fileName, long lineNumber) {<br>std::cout "FAILURE: " text " in file " fileName<br>" on line " lineNumber std::endl;<br>++nFail;<br>inline void do_test(const char* condText, bool cond, const char* fileName, long lineNumber) {<br>if (!cond)<br>do_fail(condText, fileName, lineNumber);<br>else<br>++nPass;<br>inline void succeed_() noexcept {<br>++nPass;<br>inline void report_() {<br>std::cout "\nTest Report:\n\n";<br>std::cout "\tNumber of Passes = " nPass std::endl;<br>std::cout "\tNumber of Failures = " nFail std::endl;
#define test_(cond) do_test(#cond, cond, __FILE__, __LINE__)<br>#define fail_(expr) do_fail(expr, __FILE__, __LINE__)<br>#define throw_(expr,T) \<br>try { \<br>expr; \<br>std::cout "THROW "; \<br>do_fail(#expr,__FILE__,__LINE__); \<br>} catch (const T&) { \<br>++nPass; \<br>} catch (...) { \<br>std::cout "THROW "; \<br>do_fail(#expr,__FILE__,__LINE__); \
#define nothrow_(expr) \<br>try { \<br>expr; \<br>++nPass; \<br>} catch (...) { \<br>std::cout "NOTHROW "; \<br>do_fail(#expr,__FILE__,__LINE__); \
Generally users only have to call the test_ macro, which<br>captures the expression being tested as text along with its associated<br>file name and line number. For example, if a source line is
test_(stk.top() == 1);
then the preprocessor replaces it with the following text in the<br>compilation stream:
do_test("stk.top() == 1", stk.top() == 1, "tstack.cpp", 17);
indicating that the name of the file is tstack.cpp and<br>the call occurred on line 17 of that file. If the test fails then the<br>followed is printed to the console:
FAILURE: stk.top() == 1 in file tstack.cpp on line 17<br>The report_ function prints the number of success and<br>failures, for example:
Test Report:
Number of Passes = 13<br>Number of Failures = 0<br>The other functions exist for completeness but are rarely needed by<br>users.
In this article I will fix the two problems identified at the end of<br>Part 1:
The counters in the anonymous namespace are specific to each<br>individual file. This was by design to avoid global variables and<br>because this framework was meant to be used in single-file student<br>projects, but is an unnecessary constraint; a large project should share<br>the total counts of successes and failures across all project files<br>without violating the<br>One<br>Definition Rule (ODR).
Dependencies on header files have long been recognized as a source<br>of headaches in C++. The macros above call inline functions contained in<br>the anonymous namespace, so each file under test gets it own copy of the<br>code. Modules were introduced in C++20 to alleviate such issues.
It turns out that macros will still be needed here, so I will take a<br>hybrid approach to move as much as possible into a module.
Inline Variables
C++17 introduced the notion of inline<br>variables. Just as with functions, inline<br>variables may be defined in multiple translation units, and the linker<br>is required to collapse all those definitions into one. The rules mirror<br>those for inline functions:
All definitions must be identical (same tokens, same types, same<br>initializer).
The variable has external linkage by default at namespace<br>scope.
It is guaranteed to be the same object across all translation units<br>(same address everywhere).
The fix here is to choose a named namespace and declare<br>nPass and nFail to be inline:
namespace TestFramework {<br>inline std::size_t nPass = 0;<br>inline std::size_t nFail = 0;
// Other functions reside in the same namespace...<br>inline void fail...<br>inline void test...<br>inline void succeed...<br>inline void report...
Since the functions are all in the TestFramework<br>namespace, I have renamed do_fail to fail and<br>do_test to test. I have also removed trailing<br>underscores in the last two functions. Only the macros retain the<br>trailing underscores.
That was easy. The variables satisfy the ODR and don’t pollute the<br>global namespace.
Module Migration
The main motivation for using macros was to capture the expression<br>being tested as a string. I know of no substitute for this, so the<br>macros test_, fail_, throw_, and<br>nothrow_ remain. The only difference is that they will use<br>a fully qualified call to the associated functions in the<br>TestFramework namespace, as in:
#define test_(cond) TestFramework::test(cond, #cond)
To capture the file name and line number I use<br>source_location<br>which came with C++20:
inline void test(<br>bool cond,<br>std::string_view expr,<br>const...