C++ · Course navigation

C++: a small unit-test harness

Check results and expected exceptions with a macro and a template, and report failures.

On this page
Before you start

C++ exceptions, templates, and lambdas.

  • Check results and expected exceptions with a macro and a template, and report failures.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

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.

Code example

C++
#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";
}
Output
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

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1#include <iostream>
2#include <stdexcept>
3#include <string>
4
5int failures = 0;
6
7#define CHECK(cond) \
8 do { \
9 if (cond) { \
10 std::cout << "PASS " #cond "\n"; \
11 } else { \
12 std::cout << "FAIL " #cond "\n"; \
13 ++failures; \
14 } \
15 } while (0)
16
17template <typename E, typename F>
18bool throws(F fn) {
19 try {
20 fn();
21 } catch (const E&) {
22 return true;
23 } catch (...) {
24 return false;
25 }
26 return false;
27}
28
29int parse_percent(const std::string& text) {
30 int value = std::stoi(text);
31 if (value < 0 || value > 100) {
32 throw std::out_of_range("percent out of range");
33 }
34 return value;
35}
36
37int main() {
38 CHECK(parse_percent("42") == 42);
39 CHECK(parse_percent("0") == 0);
40 CHECK(throws<std::out_of_range>([] { parse_percent("101"); }));
41 CHECK(throws<std::invalid_argument>([] { parse_percent("abc"); }));
42 std::cout << "failures: " << failures << "\n";
43}

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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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

Behind the scenes

How C++ processes this code

The C++ compiler produces a native executable. Execution enters main, where objects and function calls perform the work.

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.

Further reading