Skip to content
luke.biel edited this page Jul 21, 2022 · 8 revisions

Test case body can be defined as so: #[test_case(inputs (=> modifiers output_matcher)? (comment)?)]

Inputs

Only mandatory part of test_case macro. Number of inputs must match number of tested function arguments and be greater than 0:

#[test_case(1 ; "valid")]
#[test_case(0, 3 ; "invalid")]
#[test_case(; "also invalid)]
fn is_odd(i: i32) {
    assert_eq!(i % 2, 1);
}

Type of input is determined by argument of tested function located at the same position. It has to be implicitly castable.

A test_case with only inputs is called simple. Simple test cases allow tested function to return results similar to #[test] macro, eg.:

#[test_case("input")]
fn is_5_chars_long(s: &str) -> Result<(), Box<dyn Error>> {
    if s.len() == 5 {
        Ok(())
    } else {
        Err("Isn't 5 characters long".to_string().into())
    }
}

Output

Test case provides simple validation mechanism that lets users skip writting manual assertions within test body. Test cases that have output part present are called complex. Complex test case syntax looks as follows: => modifiers output_matcher. Turbofish operator (=>) is mandatory.

Modifiers

Modifiers allow changing test case behavior.

Inconclusive/Ignore

Inconclusive modifier indicates that test case should be skipped. It's equivalent to using #[ignore] attribute on normal #[test]. Both ignore and inconclusive keywords can be used to indicate this behavior. Example usage:

#[test_case(9 => ignore 3)]
#[test_case(4 => inconclusive 2)]
fn sqrt(number: u64) -> u64 {
    todo!() // Don't run tests until code gets implemented
}

Ignore supports supplying reason as #[ignore("reason")] attribute would. Syntax is:

#[test_case(_ => ignore["reason"] _)]

or

#[test_case(_ => inconclusive["reason"] _)]

Output matcher

There's number of syntaxes that can be used for return value validation.

Equality comparison

Simpliest of the bunch. It uses assert_eq!() under the hood to compare value returned by function with expected value. Eg.:

#[test_case(2 => true)]
#[test_case(3 => true)]
#[test_case(0 => ignore true)]
fn is_natural(number: i32) -> bool {
    number >= 0
}

Match comparison

Uses matches keyword to compare with non-Eq types. Additional if condition is allowed, similar to how match arms work in the language. Eg.:

#[test_case(3 => matches Ok(3))]
#[test_case(4 => matches Ok(_))]
#[test_case(5 => matches Ok(v) if v == 8)]
#[test_case(-1 => matches Err(_))]
fn fibbonacci(seq_idx: i32) -> Result<i32, ()> {
    if seq_idx < 0 {
        Err(())
    } else if seq_idx < 2 {
        Ok(1)
    } else {
        Ok(fibbonacci(seq_idx - 1).unwrap() + fibonacci(seq_idx - 2).unwrap())
    }
}

Panic validator

Using panics keyword we can test whether tested function panics with expected input. Additional string argument is allowed to compare against thrown error. This validator uses #[should_panic] under the hood. String argument is provided via #[should_panic(expected = #arg)]. Eg.:

#[test_case(2.0, 0.0 => panics)]
#[test_case(2.0, -0.0 => panics "Division by zero")]
#[test_case(2.0, 1.0 => 2.0)]
fn div(dividend: f32, divisor: f32) -> f32 {
    if abs(divisor) < f32::EPSILON {
        panic!("Division by zero")
    }
    dividend / divisor
}

Closure validator

Test case allows to customize result validation via with keyword. In such scenario user can define a closure that accepts actual value and perform assertion on it. Eg.:

#[test_case(2.0 => 0.0)]
#[test_case(0.0 => with |i: f64| assert!(i.is_nan()))]
fn test_division(i: f64) -> f64 {
    0.0 / i
}

Currently closure must contain an assertion and return nothing (())

Function validator

using keyword can be used to specify validation function. Such function can be either a path or an expression returning a closure acceptin single argument of type matching one returned by test. Eg.:

fn simple_validate(actual: u64) {
    assert_eq!(actual, 2)
}

fn wrapped_pretty_assert(expected: u64) -> impl Fn(u64) {
    move |actual: u64| { pretty_assertions::assert_eq!(actual, expected) }
}

#[test_case(2 => using simple_validate)]
#[test_case(1 => using wrapped_pretty_assert(1))]
fn pretty_assertions_usage(input: u64) -> u64 {
    input
}

It comparison

Test case has built in syntax allowing to create human readable test expressions without need of comments. It uses it and is keywords to indicate such tests. There are numerous matchers created based on hamcrest2 crate. Both keywords can be used in next examples. Choice of which is used is based on natural language.

Comparing numbers

#[test_case(1.0 => is equal_to 2.0 ; "eq1")]
#[test_case(1.0 => is eq 2.0 ; "eq2")]
#[test_case(1.0 => is less_than 3.0 ; "lt1")]
#[test_case(1.0 => is lt 3.0 ; "lt2")]
#[test_case(1.0 => is greater_than 0.0 ; "gt1")]
#[test_case(1.0 => is gt 0.0 ; "gt2")]
#[test_case(1.0 => is less_or_equal_than 2.0 ; "leq1")]
#[test_case(1.0 => is leq 2.0 ; "leq2")]
#[test_case(1.0 => is greater_or_equal_than 1.0 ; "geq1")]
#[test_case(1.0 => is geq 1.0 ; "geq2")]
#[test_case(1.0 => is almost_equal_to 2.1 precision 0.15 ; "almost_eq1")]
#[test_case(1.0 => is almost 2.0 precision 0.01 ; "almost_eq2")]
fn complex_tests(input: f64) -> f64 {
    input * 2.0
}

Asserting filesystem

#[test_case("Cargo.toml" => is existing_path)]
#[test_case("src/lib.rs" => is file)]
#[test_case("src/" => is dir ; "short_dir")]
#[test_case("src/" => is directory ; "long_dir")]
fn create_path(val: &str) -> std::path::PathBuf {
    std::path::PathBuf::from(val)
}

Testing containers

#[test_case(vec![1, 2, 3, 4] => it contains 1)]
#[test_case(vec![1, 2, 3, 4] => it contains_in_order [3, 4])]
fn contains_tests(items: Vec<u64>) -> Vec<u64> {
    items
}

Logic operators

Aforementioned validators can be combined using not, and and or keywords. There's caveat when it comes to and and or. As test case parser has no operator precendence built-in and & or cannot be mixed together unless they are grouped with parenthesis. Eg.:

#[test_case(vec![1, 2, 3] => it (contains 1 or contains 4) and contains 2)]
#[test_case(vec![1, 2, 3] => it (contains 1 or contains 4) and not contains 7)]
#[test_case(vec![1, 2, 3] => it contains 1 and contains 2 and contains_in_order [2, 3])]
#[test_case(vec![1, 2, 3] => it (contains 6 and contains 7) or (contains 1 and contains_in_order [1, 2, 3]))]
fn combinators_with_arrays(a: Vec<u8>) -> Vec<u8> {
    a
}

Comment

Comment is an optional string literal in the form of ; "text" at the end of test case expression. Comment currently serves two purposes: