-
Notifications
You must be signed in to change notification settings - Fork 39
Syntax
Test case body can be defined as so:
#[test_case(inputs (=> modifiers output_matcher)? (comment)?)]
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())
}
}
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 allow changing test case behavior.
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"] _)]
There's number of syntaxes that can be used for return value validation.
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
}
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())
}
}
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
}
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 (
()
)
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
}
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.
#[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
}
#[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)
}
#[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
}
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 is an optional string literal in the form of ; "text"
at the end of test case expression. Comment currently serves two purposes:
- it provides a description what given test does
- it allows bypasing test name generation