ExamplesBy LevelBy TopicLearning Paths
319 Intermediate

319: Error Handling in Tests

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "319: Error Handling in Tests" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Tests that call fallible functions traditionally use `unwrap()`, which panics with an unhelpful message on failure. Key difference from OCaml: 1. **Test return type**: Rust test functions can return `Result<(), E>` — the `?` operator works naturally inside them; OCaml tests return `unit`.

Tutorial

The Problem

Tests that call fallible functions traditionally use unwrap(), which panics with an unhelpful message on failure. Rust test functions can return Result<(), E>, enabling ? to propagate errors with full context. Additionally, #[should_panic(expected = "...")] attributes test that specific panics occur — completing the testing toolkit for both Result-returning and panic-producing code.

🎯 Learning Outcomes

  • • Write test functions returning Result<(), E> to use ? for clean error propagation
  • • Use #[should_panic(expected = "message")] to test expected panic behavior
  • • Use assert_eq! / assert! inside Result-returning tests for mixed assertions
  • • Recognize that returning Err from a test function causes a clean test failure with the error message
  • Code Example

    #![allow(clippy::all)]
    //! # Error Handling in Tests
    //!
    //! Tests can return Result, use ? operator, and #[should_panic].
    
    #[derive(Debug, PartialEq)]
    pub enum MathError {
        DivisionByZero,
        NegativeInput(i64),
    }
    
    impl std::fmt::Display for MathError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            match self {
                Self::DivisionByZero => write!(f, "division by zero"),
                Self::NegativeInput(n) => write!(f, "negative input: {n}"),
            }
        }
    }
    
    pub fn safe_div(a: i64, b: i64) -> Result<i64, MathError> {
        if b == 0 {
            Err(MathError::DivisionByZero)
        } else {
            Ok(a / b)
        }
    }
    
    pub fn safe_sqrt(x: i64) -> Result<u64, MathError> {
        if x < 0 {
            Err(MathError::NegativeInput(x))
        } else {
            Ok((x as f64).sqrt() as u64)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // Test returning Result with ?
        #[test]
        fn test_div_ok() -> Result<(), MathError> {
            assert_eq!(safe_div(10, 2)?, 5);
            Ok(())
        }
    
        // Traditional assertion
        #[test]
        fn test_div_zero() {
            assert_eq!(safe_div(5, 0), Err(MathError::DivisionByZero));
        }
    
        // Test returning Result
        #[test]
        fn test_sqrt_ok() -> Result<(), MathError> {
            assert_eq!(safe_sqrt(16)?, 4);
            Ok(())
        }
    
        // Match on error variant
        #[test]
        fn test_sqrt_neg() {
            assert_eq!(safe_sqrt(-9).unwrap_err(), MathError::NegativeInput(-9));
        }
    
        // Test that something panics
        #[test]
        #[should_panic]
        fn test_panics_on_unwrap() {
            safe_div(1, 0).unwrap();
        }
    
        // Test panic message
        #[test]
        #[should_panic(expected = "division by zero")]
        fn test_panic_message() {
            safe_div(1, 0).expect("division by zero");
        }
    }

    Key Differences

  • Test return type: Rust test functions can return Result<(), E> — the ? operator works naturally inside them; OCaml tests return unit.
  • Failure message: Rust test failure from Err(e) displays format!("{:?}", e); OCaml's Alcotest shows the exception message.
  • Expected panic: #[should_panic] is a compile-time annotation; OCaml's check_raises is a runtime assertion.
  • Integration: Result-returning tests integrate with ? and all Result combinators — tests read like production code.
  • OCaml Approach

    OCaml testing with Alcotest uses Alcotest.check for assertions and Alcotest.check_raises for expected exceptions. Test functions return unit and raise Alcotest.Test_error on failure:

    let test_safe_div () =
      Alcotest.(check int) "five" 5 (safe_div 10 2);
      Alcotest.check_raises "div by zero" Division_by_zero (fun () -> safe_div 1 0)
    

    Full Source

    #![allow(clippy::all)]
    //! # Error Handling in Tests
    //!
    //! Tests can return Result, use ? operator, and #[should_panic].
    
    #[derive(Debug, PartialEq)]
    pub enum MathError {
        DivisionByZero,
        NegativeInput(i64),
    }
    
    impl std::fmt::Display for MathError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            match self {
                Self::DivisionByZero => write!(f, "division by zero"),
                Self::NegativeInput(n) => write!(f, "negative input: {n}"),
            }
        }
    }
    
    pub fn safe_div(a: i64, b: i64) -> Result<i64, MathError> {
        if b == 0 {
            Err(MathError::DivisionByZero)
        } else {
            Ok(a / b)
        }
    }
    
    pub fn safe_sqrt(x: i64) -> Result<u64, MathError> {
        if x < 0 {
            Err(MathError::NegativeInput(x))
        } else {
            Ok((x as f64).sqrt() as u64)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // Test returning Result with ?
        #[test]
        fn test_div_ok() -> Result<(), MathError> {
            assert_eq!(safe_div(10, 2)?, 5);
            Ok(())
        }
    
        // Traditional assertion
        #[test]
        fn test_div_zero() {
            assert_eq!(safe_div(5, 0), Err(MathError::DivisionByZero));
        }
    
        // Test returning Result
        #[test]
        fn test_sqrt_ok() -> Result<(), MathError> {
            assert_eq!(safe_sqrt(16)?, 4);
            Ok(())
        }
    
        // Match on error variant
        #[test]
        fn test_sqrt_neg() {
            assert_eq!(safe_sqrt(-9).unwrap_err(), MathError::NegativeInput(-9));
        }
    
        // Test that something panics
        #[test]
        #[should_panic]
        fn test_panics_on_unwrap() {
            safe_div(1, 0).unwrap();
        }
    
        // Test panic message
        #[test]
        #[should_panic(expected = "division by zero")]
        fn test_panic_message() {
            safe_div(1, 0).expect("division by zero");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // Test returning Result with ?
        #[test]
        fn test_div_ok() -> Result<(), MathError> {
            assert_eq!(safe_div(10, 2)?, 5);
            Ok(())
        }
    
        // Traditional assertion
        #[test]
        fn test_div_zero() {
            assert_eq!(safe_div(5, 0), Err(MathError::DivisionByZero));
        }
    
        // Test returning Result
        #[test]
        fn test_sqrt_ok() -> Result<(), MathError> {
            assert_eq!(safe_sqrt(16)?, 4);
            Ok(())
        }
    
        // Match on error variant
        #[test]
        fn test_sqrt_neg() {
            assert_eq!(safe_sqrt(-9).unwrap_err(), MathError::NegativeInput(-9));
        }
    
        // Test that something panics
        #[test]
        #[should_panic]
        fn test_panics_on_unwrap() {
            safe_div(1, 0).unwrap();
        }
    
        // Test panic message
        #[test]
        #[should_panic(expected = "division by zero")]
        fn test_panic_message() {
            safe_div(1, 0).expect("division by zero");
        }
    }

    Deep Comparison

    error-in-tests

    See README.md for details.

    Exercises

  • Write a test that uses ? to call three fallible operations in sequence, failing with a descriptive error if any step fails.
  • Add #[should_panic(expected = "invariant violated")] tests for functions that use assert! to enforce preconditions.
  • Write a test that captures the Err value from a failing operation and uses assert_eq! on the error variant — verifying both that it failed and how it failed.
  • Open Source Repos