439: assert_matches! and Variant Assertions
Difficulty: 2 Level: Intermediate Use `matches!` and `assert_matches!` to write pattern-match assertions โ checking enum variants and guard conditions in one readable expression without manually destructuring.The Problem This Solves
Testing functions that return enums is awkward with `assert_eq!`. You either need to derive `PartialEq` on every enum (including ones where equality semantics are unclear), or you write verbose destructuring in every test:match parse("42") {
Parsed::Int(n) => assert_eq!(n, 42),
other => panic!("Expected Int(42), got {:?}", other),
}
That's five lines for one assertion. Multiply by a dozen tests and your test suite is more boilerplate than logic. It also doesn't compose โ you can't use it inside `assert!()` or `all()`.
`matches!($val, Pattern)` solves this: it's a boolean expression that evaluates to `true` if `$val` matches the pattern. You can add guards (`if n > 50`), use it in `assert!`, compose it with `.all()` over iterators, or use it in `if` conditions without creating a separate variable.
The Intuition
`matches!(expr, pattern)` is a syntactic shorthand for:match expr { pattern => true, _ => false }
The compiler desugars it exactly that way โ no overhead, no allocation, just a match expression. The `if` guard syntax works too: `matches!(x, Some(n) if n > 0)` becomes `match x { Some(n) if n > 0 => true, _ => false }`.
`assert_matches!` (stabilised in Rust 1.82) adds a panic message when the match fails, showing both the pattern and the actual value. For earlier Rust versions, `assert!(matches!(...))` gives the same effect with a slightly less informative message.
How It Works in Rust
#[derive(Debug, PartialEq)]
enum Parsed { Int(i64), Float(f64), Invalid(String) }
fn parse(s: &str) -> Parsed { /* ... */ }
// โโ matches! in assertions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
assert!(matches!(parse("42"), Parsed::Int(42)));
assert!(matches!(parse("3.14"), Parsed::Float(_))); // _ matches any value
assert!(matches!(parse("abc"), Parsed::Invalid(_)));
// โโ With guard conditions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
assert!(matches!(parse("100"), Parsed::Int(n) if n > 50));
assert!(matches!(parse("3.14"), Parsed::Float(f) if f > 3.0));
// โโ In if conditions โ no binding variable needed โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if matches!(parse("3.14"), Parsed::Float(f) if f > 3.0) {
println!("Got float > 3");
}
// โโ Composing with iterators โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let results: Vec<Result<u32, String>> = vec![Ok(1), Ok(2), Ok(3)];
assert!(results.iter().all(|r| matches!(r, Ok(n) if *n > 0)));
// โโ assert_matches! (Rust 1.82+) โ shows pattern in failure message โโโโโโโโโโ
// #![feature(assert_matches)] // or stable in 1.82+
// use std::assert_matches::assert_matches;
// assert_matches!(parse("42"), Parsed::Int(_)); // better failure message
// โโ Pattern matching without extracting the value โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
fn is_valid(s: &str) -> bool {
!matches!(parse(s), Parsed::Invalid(_))
}
What This Unlocks
- Enum-returning function tests โ assert on shape without deriving `PartialEq` on the whole enum or writing multi-line destructuring.
- Iterator assertions โ `assert!(results.iter().all(|r| matches!(r, Ok(_))))` โ concise whole-collection checks.
- Readable conditionals โ `if matches!(state, State::Ready | State::Idle)` is clearer than nested `if let` chains for multi-variant checks.
Key Differences
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Pattern-match assertion | `OUnit: assert_equal` with custom equality; or `match ... | _ -> assert_failure` | `matches!` macro โ inline boolean; `assert_matches!` in 1.82+ | |
| Guard in assertion | Inline `when` clause in pattern | `matches!(x, Pat if guard)` | ||
| Composing with iterators | `List.for_all (fun x -> match x with ...) lst` | `.all( | x | matches!(x, Pattern))` |
| Partial match (ignore variant data) | `match x with Foo _ -> true | _ -> false` | `matches!(x, Foo(_))` |
// 439. assert_matches! and variants
#[derive(Debug, PartialEq)]
enum Parsed { Int(i64), Float(f64), Invalid(String) }
fn parse(s: &str) -> Parsed {
if let Ok(n) = s.parse::<i64>() { Parsed::Int(n) }
else if let Ok(f) = s.parse::<f64>() { Parsed::Float(f) }
else { Parsed::Invalid(s.to_string()) }
}
fn parse_positive(s: &str) -> Result<u32, String> {
s.parse::<u32>().map_err(|e| e.to_string())
}
fn main() {
let r = parse("42");
// matches! โ inline pattern check, no allocation
assert!(matches!(r, Parsed::Int(n) if n == 42), "expected Int(42)");
// Use in conditional
if matches!(parse("3.14"), Parsed::Float(f) if f > 3.0) {
println!("Got float > 3");
}
// assert_matches! equivalent (use matches! + assert!)
let results = vec![
parse_positive("1"),
parse_positive("2"),
parse_positive("3"),
];
assert!(results.iter().all(|r| matches!(r, Ok(n) if *n > 0)));
println!("All assertions passed!");
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn test_matches_int() { assert!(matches!(parse("7"), Parsed::Int(7))); }
#[test] fn test_matches_float() { assert!(matches!(parse("1.5"), Parsed::Float(_))); }
#[test] fn test_matches_guard() { assert!(matches!(parse("100"), Parsed::Int(n) if n > 50)); }
#[test] fn test_matches_err() { assert!(matches!(parse_positive("-1"), Err(_))); }
#[test] fn test_matches_ok() { assert!(matches!(parse_positive("5"), Ok(5))); }
}
(* 439. assert_matches! style โ OCaml *)
let assert_matches ~msg pred v =
if not (pred v) then failwith msg
let parse_positive s =
match int_of_string_opt s with
| Some n when n > 0 -> Ok n
| Some _ -> Error "not positive"
| None -> Error "not a number"
let () =
(* Pattern-match assertions *)
(match parse_positive "42" with
| Ok n when n > 0 -> Printf.printf "Ok positive: %d\n" n
| _ -> failwith "expected Ok positive");
assert_matches ~msg:"should be Error"
(function Error _ -> true | _ -> false)
(parse_positive "-5");
(* matches! equivalent: check shape without unwrapping *)
let is_ok = function Ok _ -> true | _ -> false in
assert (is_ok (parse_positive "10"));
Printf.printf "All assertions passed!\n"