Attribute Macro test_casing::test_casing

source ·
#[test_casing]
Expand description

Flattens a parameterized test into a collection of test cases.

§Inputs

This attribute must be placed on a freestanding function with 1..8 arguments. The attribute must be invoked with 2 values:

  1. Number of test cases, a number literal
  2. A case iterator expression evaluating to an implementation of IntoIterator with Debuggable, 'static items. If the target function has a single argument, the iterator item type must equal to the argument type. Otherwise, the iterator must return a tuple in which each item corresponds to the argument with the same index.

A case iterator expression may reference the environment (e.g., it can be a name of a constant). It doesn’t need to be a constant expression (e.g., it may allocate in heap). It should return at least the number of items specified as the first attribute argument, and can return more items; these additional items will not be tested.

§Mapping arguments

To support more idiomatic signatures for parameterized test functions, it is possible to map from the type returned by the case iterator. The only supported kind of mapping so far is taking a shared reference (i.e., T&T). The mapping is enabled by placing the #[map(ref)] attribute on the corresponding argument. Optionally, the reference &T can be further mapped with a function / method (e.g., &String&str with String::as_str()). This is specified as #[map(ref = path::to::method)], a la serde transforms.

§Examples

§Basic usage

test_casing macro accepts 2 args: number of cases and the iterator expression. The latter can be any valid Rust expression.

#[test_casing(5, 0..5)]
// #[test] attribute is optional and is added automatically
// provided that the test function is not `async`.
fn number_is_small(number: i32) {
    assert!(number < 10);
}

Functions returning Results are supported as well.

use std::error::Error;

#[test_casing(3, ["0", "42", "-3"])]
fn parsing_numbers(s: &str) -> Result<(), Box<dyn Error>> {
    let number: i32 = s.parse()?;
    assert!(number.abs() < 100);
    Ok(())
}

The function on which the test_casing attribute is placed can be accessed from other code (e.g., for more tests):

#[test_casing(3, ["0", "42", "-3"])]
fn parsing_numbers(s: &str) -> Result<(), Box<dyn Error>> {
    // snipped...
}

#[test]
fn parsing_number_error() {
    assert!(parsing_numbers("?").is_err());
}

§Case expressions

Case expressions can be extracted to a constant for reuse or better code structuring.

const CASES: TestCases<(String, i32)> = cases! {
    [0, 42, -3].map(|i| (i.to_string(), i))
};

#[test_casing(3, CASES)]
fn parsing_numbers(s: String, expected: i32) {
    let parsed: i32 = s.parse().unwrap();
    assert_eq!(parsed, expected);
}

This example also shows that semantics of args is up to the writer; some of the args may be expected values, etc.

§Cartesian product

One of possible case expressions is a Product; it can be used to generate test cases as a Cartesian product of the expressions for separate args.

#[test_casing(6, Product((0_usize..3, ["foo", "bar"])))]
fn numbers_and_strings(number: usize, s: &str) {
    assert!(s.len() <= number);
}

§Reference args

It is possible to go from a generated argument to its reference by adding a #[map(ref)] attribute on the argument. The attribute may optionally specify a path to the transform function from the reference to the desired type (similar to transform specifications in the serde attr).

const CASES: TestCases<(String, i32)> = cases! {
    [0, 42, -3].map(|i| (i.to_string(), i))
};

#[test_casing(3, CASES)]
fn parsing_numbers(#[map(ref)] s: &str, expected: i32) {
    // Snipped...
}

#[test_casing(3, CASES)]
fn parsing_numbers_too(
    #[map(ref = String::as_str)] s: &str,
    expected: i32,
) {
    // Snipped...
}

§ignore and should_panic attributes

ignore or should_panic attributes can be specified below the test_casing attribute. They will apply to all generated tests.

#[test_casing(3, ["not", "implemented", "yet"])]
#[ignore = "Promise this will work sometime"]
fn future_test(s: &str) {
    unimplemented!()
}

#[test_casing(3, ["not a number", "-", ""])]
#[should_panic(expected = "ParseIntError")]
fn string_conversion_fail(bogus_str: &str) {
    bogus_str.parse::<i32>().unwrap();
}

§Async tests

test_casing supports all kinds of async test wrappers, such as async_std::test, tokio::test, actix::test etc. The corresponding attribute just needs to be specified below the test_casing attribute.

#[test_casing(3, ["0", "42", "-3"])]
#[async_std::test]
async fn parsing_numbers(s: &str) -> Result<(), Box<dyn Error>> {
    assert!(s.parse::<i32>()?.abs() < 100);
    Ok(())
}