ExamplesBy LevelBy TopicLearning Paths
755 Fundamental

755-testing-error-paths — Testing Error Paths

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "755-testing-error-paths — Testing Error Paths" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Happy-path tests are necessary but insufficient. Key difference from OCaml: 1. **Pattern matching**: Rust's `assert!(matches!(r, Err(ParseError::TooLong { len: 11, .. })))` is concise; OCaml uses `match r with Error (TooLong 11)

Tutorial

The Problem

Happy-path tests are necessary but insufficient. Error paths — malformed input, out-of-range values, empty fields, resource exhaustion — are where bugs hide and security vulnerabilities lurk. Testing error paths requires asserting on specific error variants, not just that an error occurred. Rust's Result and rich error enums make error path testing natural and exhaustive, unlike exception-based languages where error type testing requires awkward catch-and-inspect patterns.

🎯 Learning Outcomes

  • • Assert specific Err variants using assert!(matches!(result, Err(ParseError::Empty))) or exhaustive pattern matching
  • • Test every error variant in a ParseError enum
  • • Verify error Display messages contain user-friendly text
  • • Write boundary tests that probe the edges of valid/invalid ranges
  • • Use assert_eq! on Result values directly when PartialEq is derived
  • Code Example

    #[derive(Debug, PartialEq, Clone)]
    pub enum ParseError {
        Empty,
        TooLong { len: usize, max: usize },
        InvalidChar { ch: char, pos: usize },
    }

    Key Differences

  • Pattern matching: Rust's assert!(matches!(r, Err(ParseError::TooLong { len: 11, .. }))) is concise; OCaml uses match r with Error (TooLong 11) -> () | _ -> assert_failure "expected TooLong".
  • Exhaustiveness: Both languages enforce exhaustive matching on error variants, so adding a new variant forces test updates.
  • Partial equality: Rust's #[derive(PartialEq)] enables assert_eq!(parse(""), Err(ParseError::Empty)); OCaml requires custom equality functions.
  • Error chaining: Rust's anyhow/thiserror support error wrapping chains; OCaml's Error_monad (Tezos) does the same.
  • OCaml Approach

    OCaml error testing uses match on result values or Alcotest.check (Alcotest.result ...). QCheck generates random inputs for error paths, not just happy paths. OCaml's variant pattern matching is exhaustive: if you add a new error variant, the compiler forces you to handle it in all match expressions including tests. Result.get_error and Result.is_error provide imperative-style checks when pattern matching is verbose.

    Full Source

    #![allow(clippy::all)]
    //! # Testing Error Paths
    //!
    //! Testing error cases and unwrap discipline.
    
    /// Parse errors with detailed information
    #[derive(Debug, PartialEq, Clone)]
    pub enum ParseError {
        Empty,
        TooLong { len: usize, max: usize },
        InvalidChar { ch: char, pos: usize },
        OutOfRange { value: i64, min: i64, max: i64 },
    }
    
    impl std::fmt::Display for ParseError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            match self {
                ParseError::Empty => write!(f, "input is empty"),
                ParseError::TooLong { len, max } => write!(f, "too long: {} > {}", len, max),
                ParseError::InvalidChar { ch, pos } => write!(f, "invalid char {:?} at {}", ch, pos),
                ParseError::OutOfRange { value, min, max } => {
                    write!(f, "{} out of range [{}, {}]", value, min, max)
                }
            }
        }
    }
    
    /// Parse a string to a positive u32
    pub fn parse_positive(s: &str) -> Result<u32, ParseError> {
        if s.is_empty() {
            return Err(ParseError::Empty);
        }
        if s.len() > 10 {
            return Err(ParseError::TooLong {
                len: s.len(),
                max: 10,
            });
        }
        for (pos, ch) in s.char_indices() {
            if !ch.is_ascii_digit() {
                return Err(ParseError::InvalidChar { ch, pos });
            }
        }
        let n: u64 = s.parse().unwrap();
        if n == 0 || n > u32::MAX as u64 {
            return Err(ParseError::OutOfRange {
                value: n as i64,
                min: 1,
                max: u32::MAX as i64,
            });
        }
        Ok(n as u32)
    }
    
    /// Safe division
    pub fn divide(a: i64, b: i64) -> Result<i64, &'static str> {
        if b == 0 {
            Err("cannot divide by zero")
        } else {
            Ok(a / b)
        }
    }
    
    /// Get the first element of a slice
    pub fn head<T: Clone>(v: &[T]) -> Result<T, &'static str> {
        v.first().cloned().ok_or("empty slice")
    }
    
    /// Get the last element of a slice
    pub fn tail<T: Clone>(v: &[T]) -> Result<T, &'static str> {
        v.last().cloned().ok_or("empty slice")
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_positive_valid() {
            assert_eq!(parse_positive("123"), Ok(123));
            assert_eq!(parse_positive("1"), Ok(1));
            assert_eq!(parse_positive("4294967295"), Ok(u32::MAX));
        }
    
        #[test]
        fn test_parse_positive_empty() {
            assert_eq!(parse_positive(""), Err(ParseError::Empty));
        }
    
        #[test]
        fn test_parse_positive_too_long() {
            let result = parse_positive("12345678901");
            assert_eq!(result, Err(ParseError::TooLong { len: 11, max: 10 }));
        }
    
        #[test]
        fn test_parse_positive_invalid_char() {
            assert_eq!(
                parse_positive("12x4"),
                Err(ParseError::InvalidChar { ch: 'x', pos: 2 })
            );
        }
    
        #[test]
        fn test_parse_positive_zero() {
            let result = parse_positive("0");
            assert!(matches!(result, Err(ParseError::OutOfRange { .. })));
        }
    
        #[test]
        fn test_divide_success() {
            assert_eq!(divide(10, 2), Ok(5));
            assert_eq!(divide(-10, 2), Ok(-5));
        }
    
        #[test]
        fn test_divide_by_zero() {
            assert_eq!(divide(10, 0), Err("cannot divide by zero"));
        }
    
        #[test]
        fn test_head_success() {
            assert_eq!(head(&[1, 2, 3]), Ok(1));
        }
    
        #[test]
        fn test_head_empty() {
            assert_eq!(head::<i32>(&[]), Err("empty slice"));
        }
    
        #[test]
        fn test_tail_success() {
            assert_eq!(tail(&[1, 2, 3]), Ok(3));
        }
    
        #[test]
        fn test_error_display() {
            let err = ParseError::InvalidChar { ch: 'x', pos: 5 };
            assert_eq!(format!("{}", err), "invalid char 'x' at 5");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_positive_valid() {
            assert_eq!(parse_positive("123"), Ok(123));
            assert_eq!(parse_positive("1"), Ok(1));
            assert_eq!(parse_positive("4294967295"), Ok(u32::MAX));
        }
    
        #[test]
        fn test_parse_positive_empty() {
            assert_eq!(parse_positive(""), Err(ParseError::Empty));
        }
    
        #[test]
        fn test_parse_positive_too_long() {
            let result = parse_positive("12345678901");
            assert_eq!(result, Err(ParseError::TooLong { len: 11, max: 10 }));
        }
    
        #[test]
        fn test_parse_positive_invalid_char() {
            assert_eq!(
                parse_positive("12x4"),
                Err(ParseError::InvalidChar { ch: 'x', pos: 2 })
            );
        }
    
        #[test]
        fn test_parse_positive_zero() {
            let result = parse_positive("0");
            assert!(matches!(result, Err(ParseError::OutOfRange { .. })));
        }
    
        #[test]
        fn test_divide_success() {
            assert_eq!(divide(10, 2), Ok(5));
            assert_eq!(divide(-10, 2), Ok(-5));
        }
    
        #[test]
        fn test_divide_by_zero() {
            assert_eq!(divide(10, 0), Err("cannot divide by zero"));
        }
    
        #[test]
        fn test_head_success() {
            assert_eq!(head(&[1, 2, 3]), Ok(1));
        }
    
        #[test]
        fn test_head_empty() {
            assert_eq!(head::<i32>(&[]), Err("empty slice"));
        }
    
        #[test]
        fn test_tail_success() {
            assert_eq!(tail(&[1, 2, 3]), Ok(3));
        }
    
        #[test]
        fn test_error_display() {
            let err = ParseError::InvalidChar { ch: 'x', pos: 5 };
            assert_eq!(format!("{}", err), "invalid char 'x' at 5");
        }
    }

    Deep Comparison

    OCaml vs Rust: Testing Error Paths

    Error Type Definition

    Rust

    #[derive(Debug, PartialEq, Clone)]
    pub enum ParseError {
        Empty,
        TooLong { len: usize, max: usize },
        InvalidChar { ch: char, pos: usize },
    }
    

    OCaml

    type parse_error =
      | Empty
      | Too_long of { len: int; max: int }
      | Invalid_char of { ch: char; pos: int }
    

    Testing Specific Error Variants

    Rust

    #[test]
    fn test_empty_error() {
        assert_eq!(parse_positive(""), Err(ParseError::Empty));
    }
    
    #[test]
    fn test_invalid_char_error() {
        assert_eq!(
            parse_positive("12x4"),
            Err(ParseError::InvalidChar { ch: 'x', pos: 2 })
        );
    }
    

    OCaml

    let%test "empty error" =
      parse_positive "" = Error Empty
    
    let%test "invalid char error" =
      parse_positive "12x4" = Error (Invalid_char { ch = 'x'; pos = 2 })
    

    Pattern Matching on Errors

    Rust

    assert!(matches!(result, Err(ParseError::OutOfRange { .. })));
    

    OCaml

    match result with
    | Error (Out_of_range _) -> true
    | _ -> false
    

    Key Differences

    AspectOCamlRust
    Error comparisonStructural equalityPartialEq derive
    Wildcard match_.. for struct fields
    Error displayManual to_stringDisplay trait
    Test assertion=assert_eq!

    Exercises

  • Add a TooShort { len: usize, min: usize } error variant to ParseError and write tests for the minimum length boundary. Update all Display and test code.
  • Write a table-driven test that covers 20+ cases for parse_positive using a Vec<(&str, Result<u32, ParseError>)> — verify each pair in a loop.
  • Add a DeprecatedFormat error that is non-fatal (produces a warning but returns Ok), and write tests that verify the result contains both the value and the warning.
  • Open Source Repos