test-part1
Automated Unit<br>Testing On-The-Cheap: Part 1
C++ Code Capsules
When Extreme Programming was all the rage in the late 1990s,<br>automated unit testing was still a relatively fresh idea for many<br>working software developers. JUnit and CppUnit emerged as the leading<br>test frameworks for Java and C++, but I wanted something even<br>simpler—code that students could understand and that was easy to<br>use.
I thought I’d achieved it in the well-received article I wrote for<br>the C/C++ Users journal in September 2000,<br>The<br>Simplest Automated Unit Test Framework That Could Possibly Work. But<br>I hadn’t. A few years later, while teaching and developing examples at<br>Utah Valley University, I found myself wanting something even<br>smaller. How small could I go? Did I really need inheritance? My<br>freshman C++ students didn’t learn object-oriented programming right<br>away anyway, but they needed to learn the importance of crafting robust<br>code from the get-go, regardless of paradigm.
The result was a simple header file: test.h.
Minimal Requirements
Individual tests need to be boolean expressions, of course, so<br>successes will be noted and failures should be flagged with their<br>location and the expression that failed. Code that generates exceptions<br>should also be validated. Capturing the source text of an expression<br>calls for the preprocessor’s stringizing operator (see<br>#cond below). (For a deep dive into the macro preprocessor<br>see<br>The<br>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<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. The function<br>do_test tests the condition and responds accordingly:
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;
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;
If the condition failed, the following would be reported:
FAILURE: stk.top() == 1 in file tstack.cpp on line 17
Handling Exceptions
Code that expects an exception to be thrown under certain conditions<br>can be verified by the following function macro:
#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__); \
The Stack class I am using for this example wraps<br>std::stack with member functions that throw<br>std::underflow_error when calling top or<br>pop on an empty stack (std::stack does not<br>throw). The following code tests proper exception handling:
Stackint> stk;
test_(stk.size() == 0);
// Test exceptions (top and pop are invalid on empty stack)<br>throw_(stk.top(), std::underflow_error);<br>throw_(stk.pop(), std::underflow_error);<br>nothrow_(stk.size());
The Code
For completeness there are also succeed_,<br>fail_, and nothrow_ macros. There is also a<br>report_ function. Here is the complete header file:
#ifndef TEST_H<br>#define TEST_H<br>#include<br>#include<br>using std::size_t;
// Unit Test Scaffolding: Users call test_, fail_, succeed_, throw_, nothrow_, and report_<br>// AUTHOR: Chuck Allison (Creative Commons License, 2001 - 2017)
namespace {<br>size_t nPass{0};<br>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__); \<br>#endif
Here is the driver for the Stack class:
// tstack.cpp: Test driver for Stack<br>#include "test.h"<br>#include "stack.h"
int main() {<br>Stackint> stk;
test_(stk.size() == 0);
// Test exceptions (top and pop are invalid on empty stack)<br>throw_(stk.top(), std::underflow_error);<br>throw_(stk.pop(), std::underflow_error);<br>nothrow_(stk.size());
// Test push and top<br>stk.push(1);<br>test_(stk.top() == 1);<br>test_(stk.size() ==...