437: Test Helper Macros
Tutorial Video
Text description (accessibility)
This video demonstrates the "437: Test Helper Macros" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Test code benefits from higher-level assertions than `assert_eq!`. Key difference from OCaml: 1. **Panic vs. exception**: Rust's `assert_panics!` uses `catch_unwind`; OCaml's `assert_raises` is a function that catches exceptions — both serve the same purpose.
Tutorial
The Problem
Test code benefits from higher-level assertions than assert_eq!. Floating-point tests need approximate equality, panic testing needs catch_unwind boilerplate, and table-driven tests need repetition over input/output pairs. Test helper macros eliminate this repetition: assert_approx!(a, b), assert_panics!(expr), test_cases!(fn, input => expected, ...). These make tests more readable, reduce boilerplate, and ensure consistent error messages when assertions fail.
Test helper macros are widespread: approx crate for float comparison, proptest for property-based testing, rstest for parametrized tests, and countless internal test utilities.
🎯 Learning Outcomes
assert_approx! uses a default epsilon with override capabilitystd::panic::catch_unwind enables testing that code panicstest_cases! as a lightweight parametrized test approachCode Example
#![allow(clippy::all)]
//! Test Helper Macros
//!
//! Macros that simplify testing.
/// Assert approximately equal for floats.
#[macro_export]
macro_rules! assert_approx {
($left:expr, $right:expr) => {
assert_approx!($left, $right, 1e-6)
};
($left:expr, $right:expr, $epsilon:expr) => {
let left = $left;
let right = $right;
let diff = (left as f64 - right as f64).abs();
assert!(
diff < $epsilon,
"assertion failed: `{} ≈ {}` (diff: {})",
left,
right,
diff
);
};
}
/// Assert that expression panics.
#[macro_export]
macro_rules! assert_panics {
($body:expr) => {{
let result = std::panic::catch_unwind(|| $body);
assert!(result.is_err(), "Expected panic but none occurred");
}};
}
/// Test multiple inputs.
#[macro_export]
macro_rules! test_cases {
($func:expr, $($input:expr => $expected:expr),+ $(,)?) => {
$(assert_eq!($func($input), $expected);)+
};
}
pub fn double(x: i32) -> i32 {
x * 2
}
pub fn square(x: i32) -> i32 {
x * x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_approx_equal() {
assert_approx!(1.0, 1.0000001);
}
#[test]
#[should_panic]
fn test_approx_not_equal() {
assert_approx!(1.0, 2.0);
}
#[test]
fn test_cases_double() {
test_cases!(double,
0 => 0,
1 => 2,
5 => 10
);
}
#[test]
fn test_cases_square() {
test_cases!(square,
0 => 0,
3 => 9,
4 => 16
);
}
#[test]
fn test_panics_macro() {
assert_panics!(panic!("test"));
}
}Key Differences
assert_panics! uses catch_unwind; OCaml's assert_raises is a function that catches exceptions — both serve the same purpose.alcotest has built-in float equality with configurable precision.test_cases! expands to multiple assert_eq! calls; OCaml's List.iter provides the same with cleaner syntax.alcotest, ounit, qcheck).OCaml Approach
OCaml's alcotest library provides check float "msg" expected actual with epsilon support. OUnit2.assert_raises tests for exceptions. QCheck provides property-based testing. OCaml's ppx_expect generates inline snapshot tests. The test ecosystem is library-based rather than macro-based, but achieves the same goals. ppx_inline_test with let%test_unit "name" = ... syntax provides test helpers via PPX.
Full Source
#![allow(clippy::all)]
//! Test Helper Macros
//!
//! Macros that simplify testing.
/// Assert approximately equal for floats.
#[macro_export]
macro_rules! assert_approx {
($left:expr, $right:expr) => {
assert_approx!($left, $right, 1e-6)
};
($left:expr, $right:expr, $epsilon:expr) => {
let left = $left;
let right = $right;
let diff = (left as f64 - right as f64).abs();
assert!(
diff < $epsilon,
"assertion failed: `{} ≈ {}` (diff: {})",
left,
right,
diff
);
};
}
/// Assert that expression panics.
#[macro_export]
macro_rules! assert_panics {
($body:expr) => {{
let result = std::panic::catch_unwind(|| $body);
assert!(result.is_err(), "Expected panic but none occurred");
}};
}
/// Test multiple inputs.
#[macro_export]
macro_rules! test_cases {
($func:expr, $($input:expr => $expected:expr),+ $(,)?) => {
$(assert_eq!($func($input), $expected);)+
};
}
pub fn double(x: i32) -> i32 {
x * 2
}
pub fn square(x: i32) -> i32 {
x * x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_approx_equal() {
assert_approx!(1.0, 1.0000001);
}
#[test]
#[should_panic]
fn test_approx_not_equal() {
assert_approx!(1.0, 2.0);
}
#[test]
fn test_cases_double() {
test_cases!(double,
0 => 0,
1 => 2,
5 => 10
);
}
#[test]
fn test_cases_square() {
test_cases!(square,
0 => 0,
3 => 9,
4 => 16
);
}
#[test]
fn test_panics_macro() {
assert_panics!(panic!("test"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_approx_equal() {
assert_approx!(1.0, 1.0000001);
}
#[test]
#[should_panic]
fn test_approx_not_equal() {
assert_approx!(1.0, 2.0);
}
#[test]
fn test_cases_double() {
test_cases!(double,
0 => 0,
1 => 2,
5 => 10
);
}
#[test]
fn test_cases_square() {
test_cases!(square,
0 => 0,
3 => 9,
4 => 16
);
}
#[test]
fn test_panics_macro() {
assert_panics!(panic!("test"));
}
}
Deep Comparison
OCaml vs Rust: macro test helpers
See example.rs and example.ml for side-by-side implementations.
Key Points
Exercises
assert_matrix_eq!(m1, m2, epsilon) that compares two 2D Vec<Vec<f64>> element-wise within epsilon, printing the differing element indices and values when they differ.test_property!(fn_name: |x: i32| { /* property */ }, count: 1000) that generates 1000 random i32 values and asserts the property holds for each, printing the failing input on assertion failure.bench!(name, warmup: 100, runs: 1000, { /* code */ }) that runs the code warmup times, then runs times, computing mean and stddev of the timing, printing a summary. Use Instant::now() in the expansion.