Testing: C++ and Rust

C++ has no test runner in the language, so a project brings one or writes the smallest one it can: a macro that checks a condition and a main that counts the failures. Rust has the runner in cargo, the marker in an attribute and the assertions in the standard library. The two programs below check the same two additions.

The task in C++

The CHECK macro prints the failing condition, increments a counter and lets the program continue, so one run reports every failure. The main returns zero either way, which is the first thing a real harness has to fix.

One check passes and one fails, so the output is the failing condition and a count of one.

#include <iostream> int failures = 0; #define CHECK(condition) \ do { \ if (!(condition)) { \ std::cerr << "FAILED: " #condition "\n"; \ ++failures; \ } \ } while (0) int add(int left, int right) { return left + right; } int main() { CHECK(add(2, 2) == 4); CHECK(add(2, 2) == 5); std::cout << failures << " failures\n"; }

g++ -std=c++23 -Wall -Wextra testing.cpp -o testing
./testing

FAILED: add(2, 2) == 5
1 failures

The macro is the whole framework: a condition, a message and a counter. The lesson on unit testing without a framework builds the same runner with a function per test.

The same task in Rust

The library has the two functions, and the tests module has a test per behaviour. Cargo finds the tests, runs each in its own thread and reports the count, which is the runner the C++ program writes by hand.

The second test checks that a division by zero panics, and the attribute on it says so. The run reports three passes and no failures.

pub fn add(left: i32, right: i32) -> i32 { left + right } pub fn divide(left: i32, right: i32) -> i32 { left / right } #[cfg(test)] mod tests { use super::*; #[test] fn adds_two_numbers() { assert_eq!(add(2, 2), 4); } #[test] fn divides_exactly() { assert!(divide(10, 2) == 5, "ten divided by two must be five"); } #[test] #[should_panic] fn divides_by_zero() { divide(1, 0); } }

cargo test

Compiling calc v0.1.0 (/tmp/rw5/L065/calc)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.50s
Running unittests src/lib.rs (target/debug/deps/calc-c10086937597dec3)

running 3 tests
test tests::adds_two_numbers ... ok
test tests::divides_exactly ... ok
test tests::divides_by_zero - should panic ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests calc

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

The output names each test and then counts them. The lesson on unit tests shows a filtered run and a failing one.

What differs and why

Who finds the tests: in C++ the program lists them, and a test that is not called does not run. In Rust the attribute registers the function, and cargo discovers every registered test in the crate.

What a failure does: the CHECK macro prints and continues, so the exit status is still zero. A Rust test fails on a panic, the harness reports it and cargo exits non-zero, which a pipeline can see.

What the assertion costs: a C++ assert disappears when NDEBUG is defined, so a release run can hide a broken check. Rust's assert_eq! is compiled in every build, and the test profile is a debug build by default.

The habit that breaks

The habit is to rely on assert for a check that must always run. Compiling with -DNDEBUG removes it, and the program below reports that all tests passed with a wrong expectation in the source.

#include <cassert> #include <iostream> int add(int left, int right) { return left + right; } int main() { assert(add(2, 2) == 5); std::cout << "all tests passed\n"; }

g++ -std=c++23 -Wall -Wextra -DNDEBUG assert_off.cpp -o assert_off
./assert_off

all tests passed

The same expectation in Rust, compiled with -O, still panics, because the macro is not tied to a build flag. The message names both values.

fn add(left: i32, right: i32) -> i32 { left + right } fn main() { assert_eq!(add(2, 2), 5); println!("all tests passed"); }

rustc --edition 2024 -O assert_off.rs
./assert_off


thread 'main' (4042) panicked at assert_off.rs:6:5:
assertion `left == right` failed
left: 4
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
status: 101

The C++ lesson for this ground is Unit testing without a framework, and the Rust lesson is Unit tests with cargo test. The previous page in this set is Iterators and algorithms.

The C++ programs were built with g++ 15.2.0 and the Rust programs with rustc 1.96.1 and cargo 1.96.1, and both were tested on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev