ExamplesBy LevelBy TopicLearning Paths
437 Fundamental

437: Test Helper Macros

Functional Programming

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

  • • Understand how test macros reduce boilerplate in test suites
  • • Learn how assert_approx! uses a default epsilon with override capability
  • • See how std::panic::catch_unwind enables testing that code panics
  • • Understand test_cases! as a lightweight parametrized test approach
  • • Learn how test macros improve assertion failure messages with context
  • Code 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

  • 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.
  • Float precision: Rust needs macros for custom epsilon; OCaml's alcotest has built-in float equality with configurable precision.
  • Table tests: Rust's test_cases! expands to multiple assert_eq! calls; OCaml's List.iter provides the same with cleaner syntax.
  • Framework integration: Rust test macros work within Rust's built-in test framework; OCaml test helpers require a testing library (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"));
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Matrix assertion: Implement 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.
  • Property test macro: Create 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.
  • Benchmark macro: Implement 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.
  • Open Source Repos