C++: a small unit-test harness
Check results and expected exceptions with a macro and a template, and report failures.
#include <iostream>
#include <stdexcept>
#include <string>
int failures = 0;
#define CHECK(cond) \
do { \
if (cond) { \
std::cout << "PASS " #cond "\n"; \
} else { \
std::cout << "FAIL " #cond "\n"; \
++failures; \
} \
} while (0)
template <typename E, typename F>
bool throws(F fn) {
try {
fn();
} catch (const E&) {
return true;
} catch (...) {
return false;
}
return false;
}
int parse_percent(const std::string& text) {
int value = std::stoi(text);
if (value < 0 || value > 100) {
throw std::out_of_range("percent out of range");
}
return value;
}
int main() {
CHECK(parse_percent("42") == 42);
CHECK(parse_percent("0") == 0);
CHECK(throws<std::out_of_range>([] { parse_percent("101"); }));
CHECK(throws<std::invalid_argument>([] { parse_percent("abc"); }));
std::cout << "failures: " << failures << "\n";
}PASS parse_percent("42") == 42
PASS parse_percent("0") == 0
PASS throws<std::out_of_range>([] { parse_percent("101"); })
PASS throws<std::invalid_argument>([] { parse_percent("abc"); })
failures: 0Explanation
A unit test checks one behavior of a small piece of code. Frameworks such as GoogleTest, Catch2, and doctest provide assertions, test discovery, and reports, but their core idea is small: evaluate a condition and report if it is false.
The CHECK macro prints PASS or FAIL together with the text of the condition, using the preprocessor's # operator to turn the expression into a string. The throws template runs a callable and reports whether it threw the expected exception type. Testing the error case matters as much as testing the normal case.
Explore the dry run
#include <iostream>#include <stdexcept>#include <string> int failures = 0; #define CHECK(cond) \ do { \ if (cond) { \ std::cout << "PASS " #cond "\n"; \ } else { \ std::cout << "FAIL " #cond "\n"; \ ++failures; \ } \ } while (0) template <typename E, typename F>bool throws(F fn) { try { fn(); } catch (const E&) { return true; } catch (...) { return false; } return false;} int parse_percent(const std::string& text) { int value = std::stoi(text); if (value < 0 || value > 100) { throw std::out_of_range("percent out of range"); } return value;} int main() { CHECK(parse_percent("42") == 42); CHECK(parse_percent("0") == 0); CHECK(throws<std::out_of_range>([] { parse_percent("101"); })); CHECK(throws<std::invalid_argument>([] { parse_percent("abc"); })); std::cout << "failures: " << failures << "\n";}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| The macro takes a condition and uses #cond to print its source text. | — | — | |
| A helper that reports whether a callable throws the expected type. | — | — | |
| A normal case passes. | — | PASS parse_percent("42") == 42 | |
| 101 is outside 0 to 100, so out_of_range is thrown as expected. | — | PASS parse_percent("42") == 42
PASS parse_percent("0") == 0
PASS throws<std::out_of_range>([] { parse_percent("101"); }) | |
| All four checks passed. | failures = 0 | PASS parse_percent("42") == 42
PASS parse_percent("0") == 0
PASS throws<std::out_of_range>([] { parse_percent("101"); })
PASS throws<std::invalid_argument>([] { parse_percent("abc"); })
failures: 0 |
Cost and common mistakes
Each check is O(1) plus the cost of the code under test.
- std::stoi stops at the first invalid character, so stoi("12abc") returns 12 instead of failing. Check the position it reports if you need strictness.
- Macros do not respect scope or types, so keep them small. Frameworks provide better failure messages.
Try it yourself
Add a test for the upper boundary.
Reveal answer and reasoning
CHECK(parse_percent("100") == 100); 100 is the largest allowed value, so it must be accepted.