11.0 What This Teaches
assertfrom<cassert>for quick checks- A minimal test runner using exceptions and
std::function - A
CHECKmacro for readable failure messages - Structuring tests by function under test
- Exit codes: returning 1 when tests fail
11.1 assert
#include <cassert>
int add(int a, int b) { return a + b; }
int main() {
assert(add(2, 3) == 5); // aborts with message if false
assert(add(0, 0) == 0);
}
assert aborts the program if the condition is false, printing the
file and line number. It is disabled when NDEBUG is defined
(release builds). Use it for cheap sanity checks, not for production error
handling.
11.2 A Minimal Test Runner
#include <iostream>
#include <functional>
#include <stdexcept>
#include <vector>
#include <string>
struct TestResult { std::string name; bool passed; std::string message; };
std::vector<TestResult> results;
void run_test(const std::string& name, std::function<void()> fn) {
try {
fn();
results.push_back({name, true, ""});
} catch (const std::exception& e) {
results.push_back({name, false, e.what()});
}
}
#define CHECK(expr) \
if (!(expr)) throw std::runtime_error("CHECK failed: " #expr)
run_test catches
the exception, records the result, and continues with the next test -
one test failure does not stop the others.
11.3 Writing Tests
int clamp(int v, int lo, int hi) {
return v < lo ? lo : v > hi ? hi : v;
}
void test_clamp() {
CHECK(clamp(10, 0, 20) == 10); // in range: unchanged
CHECK(clamp(25, 0, 20) == 20); // above hi: clamped to hi
CHECK(clamp(-5, 0, 20) == 0); // below lo: clamped to lo
CHECK(clamp( 0, 0, 20) == 0); // at lo boundary
CHECK(clamp(20, 0, 20) == 20); // at hi boundary
}
CHECK failure reports which
expression failed.
11.4 Printing Results and Exit Code
int main() {
run_test("clamp: in/hi/lo", test_clamp);
// ... more tests ...
int passed = 0, failed = 0;
for (const auto& r : results) {
if (r.passed) { std::cout << "[PASS] " << r.name << "\n"; ++passed; }
else { std::cout << "[FAIL] " << r.name << ": " << r.message << "\n"; ++failed; }
}
std::cout << "\n" << passed << " passed, " << failed << " failed\n";
return failed > 0 ? 1 : 0; // non-zero exit signals failure to CI
}
11.5 Example - All Together
// Testing - simple assert-based unit tests and a minimal test runner.
#include <iostream>
#include <functional>
#include <stdexcept>
#include <vector>
#include <string>
int add(int a, int b) { return a + b; }
int clamp(int v, int lo, int hi) { return v < lo ? lo : v > hi ? hi : v; }
std::string to_upper(std::string s) {
for (char& c : s) c = static_cast<char>(std::toupper(c));
return s;
}
struct TestResult { std::string name; bool passed; std::string message; };
std::vector<TestResult> results;
void run_test(const std::string& name, std::function<void()> fn) {
try { fn(); results.push_back({name, true, ""}); }
catch (const std::exception& e) { results.push_back({name, false, e.what()}); }
}
#define CHECK(expr) \
if (!(expr)) throw std::runtime_error("CHECK failed: " #expr)
void test_add() { CHECK(add(2,3)==5); CHECK(add(-1,1)==0); }
void test_clamp() { CHECK(clamp(25,0,20)==20); CHECK(clamp(-5,0,20)==0); CHECK(clamp(10,0,20)==10); }
void test_to_upper() { CHECK(to_upper("hello")=="HELLO"); CHECK(to_upper("")==""); }
int main() {
run_test("add", test_add);
run_test("clamp", test_clamp);
run_test("to_upper", test_to_upper);
int passed = 0, failed = 0;
for (const auto& r : results) {
std::cout << (r.passed ? "[PASS] " : "[FAIL] ") << r.name;
if (!r.passed) std::cout << ": " << r.message;
std::cout << "\n";
r.passed ? ++passed : ++failed;
}
std::cout << "\n" << passed << " passed, " << failed << " failed\n";
return failed > 0 ? 1 : 0;
}
[PASS] add
[PASS] clamp
[PASS] to_upper
3 passed, 0 failed
11.6 Exercise
Exercise
- Add a function
bool is_palindrome(const std::string& s)and write tests for it covering empty strings, single characters, palindromes, and non-palindromes. - Deliberately break one of the tested functions and confirm the
[FAIL]output shows whichCHECKexpression failed. - Look up Google Test (
gtest) or Catch2 and compare theirEXPECT_EQ/REQUIREmacros to theCHECKmacro above.
11.7 Common Mistakes
assert disabled in release builds
-DNDEBUG (or in Release mode in MSVC/CMake),
all assert calls are removed. Tests using only assert
silently pass in release builds regardless of correctness. Use a proper
check mechanism in test code.
One test failure stops all tests
assert aborts,
subsequent tests never run. Wrap each test in a try/catch as shown in the
runner so a single failure does not hide others.
Not checking the exit code
main even when tests fail makes CI pipelines
see the test binary as succeeding. Always return a non-zero exit code on
failure.
11.8 Key Terms
| Term | Meaning |
|---|---|
| assert(expr) | Aborts if expr is false; disabled by NDEBUG in release builds |
| CHECK macro | Throws an exception with the failing expression; allows tests to continue |
| test runner | Code that calls each test, catches failures, and reports results |
| NDEBUG | Preprocessor macro that disables assert; set by release build configurations |
| exit code | Value returned from main; 0 = success, non-zero = failure; checked by shells and CI |
| Google Test | Popular C++ testing framework; provides EXPECT_EQ, ASSERT_EQ, test fixtures |
| Catch2 | Header-only C++ testing framework; REQUIRE and CHECK macros |