319: Error Handling in Tests
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
Result<(), E> to use ? for clean error propagation#[should_panic(expected = "message")] to test expected panic behaviorassert_eq! / assert! inside Result-returning tests for mixed assertionsErr from a test function causes a clean test failure with the error messageCode 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
Result<(), E> — the ? operator works naturally inside them; OCaml tests return unit.Err(e) displays format!("{:?}", e); OCaml's Alcotest shows the exception message.#[should_panic] is a compile-time annotation; OCaml's check_raises is a runtime assertion.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");
}
}#[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
? to call three fallible operations in sequence, failing with a descriptive error if any step fails.#[should_panic(expected = "invariant violated")] tests for functions that use assert! to enforce preconditions.Err value from a failing operation and uses assert_eq! on the error variant — verifying both that it failed and how it failed.