Site

Testing — Unit Testing

Tutorial 11.0  •  C++ / Learn

11.0 What This Teaches

Testing verifies that code behaves as specified. C++ has no built-in test framework, but you can write effective tests with standard tools and a small helper. This tutorial covers:

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)
Each test is a callable that throws on failure. 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
}
Group tests by the function they cover. Test the normal case, the boundaries, and any error conditions. Each 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 which CHECK expression failed.
  • Look up Google Test (gtest) or Catch2 and compare their EXPECT_EQ / REQUIRE macros to the CHECK macro above.

11.7 Common Mistakes

assert disabled in release builds

When compiled with -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

If tests call each other or if the first 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

Returning 0 from 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

TermMeaning
assert(expr)Aborts if expr is false; disabled by NDEBUG in release builds
CHECK macroThrows an exception with the failing expression; allows tests to continue
test runnerCode that calls each test, catches failures, and reports results
NDEBUGPreprocessor macro that disables assert; set by release build configurations
exit codeValue returned from main; 0 = success, non-zero = failure; checked by shells and CI
Google TestPopular C++ testing framework; provides EXPECT_EQ, ASSERT_EQ, test fixtures
Catch2Header-only C++ testing framework; REQUIRE and CHECK macros