Skip to main content

rstest

Attribute Macro rstest 

Source
#[rstest]
Expand description

The attribute that you should use for your tests. Your annotated function’s arguments can be injected with fixtures, provided by parametrized cases or by value lists.

rstest attribute can be applied to any function and you can customize its parameters by using function and arguments attributes.

Your test function can use generics, impl or dyn and like any kind of rust tests:

  • return results
  • marked by #[should_panic] attribute

In the function signature, where you define your tests inputs, you can also destructuring the values like any other rust function.

If the test function is an async function rstest will run all cases as async tests. However, async tests must supply either an explicit test attribute or an implicit test attribute.

In your test function you can:

Additional Attributes:

§Injecting Fixtures

The simplest case is write a test that can be injected with [fixture]s. You can just declare all used fixtures by passing them as a function’s arguments. This can help your test to be neat and make your dependency clear.

use rstest::*;

#[fixture]
fn injected() -> i32 { 42 }

#[rstest]
fn the_test(injected: i32) {
    assert_eq!(42, injected)
}

[rstest] procedural macro will desugar it to something that isn’t so far from

#[test]
fn the_test() {
    let injected=injected();
    assert_eq!(42, injected)
}

If you want to use long and descriptive names for your fixture but prefer to use shorter names inside your tests you use rename feature described in fixture rename:

use rstest::*;

#[fixture]
fn long_and_boring_descriptive_name() -> i32 { 42 }

#[rstest]
fn the_test(#[from(long_and_boring_descriptive_name)] short: i32) {
    assert_eq!(42, short)
}

The use of #[from(...)] attribute is mandatory if you need to destructure the value:

use rstest::*;

#[fixture]
fn tuple() -> (u32, f32) { (42, 42.0) }

#[rstest]
fn the_test(#[from(tuple)] (u, _): (u32, f32)) {
    assert_eq!(42, u)
}

Sometimes is useful to have some parameters in your fixtures but your test would override the fixture’s default values in some cases. Like in fixture partial injection you use #[with] attribute to indicate some fixture’s arguments also in rstest.

use rstest::*;

#[fixture]
fn user(
    #[default("Alice")] name: impl AsRef<str>,
    #[default(22)] age: u8
) -> User { User(name.as_ref().to_owned(), age) }

#[rstest]
fn check_user(#[with("Bob")] user: User) {
    assert_eq("Bob", user.name())
}

§Test Parametrized Cases

If you would execute your test for a set of input data cases you can define the arguments to use and the cases list. Let see the classical Fibonacci example. In this case we would give the input value and the expected result for a set of cases to test.

use rstest::rstest;

#[rstest]
#[case(0, 0)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
fn fibonacci_test(#[case] input: u32,#[case] expected: u32) {
    assert_eq!(expected, fibonacci(input))
}

fn fibonacci(input: u32) -> u32 {
    match input {
        0 => 0,
        1 => 1,
        n => fibonacci(n - 2) + fibonacci(n - 1)
    }
}

rstest will produce 5 independent tests and not just one that check every case. Every test can fail independently and cargo test will give follow output:

running 5 tests
test fibonacci_test::case_1 ... ok
test fibonacci_test::case_2 ... ok
test fibonacci_test::case_3 ... ok
test fibonacci_test::case_4 ... ok
test fibonacci_test::case_5 ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

The cases input values can be arbitrary Rust expressions that return the argument type.

use rstest::rstest;

fn sum(a: usize, b: usize) -> usize { a + b }

#[rstest]
#[case("foo", 3)]
#[case(String::from("foo"), 2 + 1)]
#[case(format!("foo"), sum(2, 1))]
fn test_len(#[case] s: impl AsRef<str>,#[case] len: usize) {
    assert_eq!(s.as_ref().len(), len);
}

§Feature flagged cases

In case you want certain test cases to only be present if a certain feature is enabled, use #[cfg_attr(feature = …, case(…))]:

use rstest::rstest;

#[rstest]
#[case(2, 2)]
#[cfg_attr(feature = "frac", case(4/2, 2))]
fn it_works(#[case] a: u32, #[case] b: u32) {
    assert!(a == b);
}

This also works with rstest_reuse.

§Magic Conversion

You can use the magic conversion feature every time you would define a variable where its type define FromStr trait: test will parse the string to build the value.

#[rstest]
#[case("resources/empty", 0)]
#[case("resources/divine_comedy", 101.698)]
fn test_count_words(#[case] path: PathBuf, #[case] expected: usize) {
    assert_eq!(expected, count_words(path))
}

§Optional case description

Optionally you can give a description to every case simple by follow case with ::my_case_description where my_case_description should be a a valid Rust ident.

#[rstest]
#[case::zero_base_case(0, 0)]
#[case::one_base_case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
fn fibonacci_test(#[case] input: u32,#[case] expected: u32) {
    assert_eq!(expected, fibonacci(input))
}

Output will be

running 4 tests
test fibonacci_test::case_1_zero_base_case ... ok
test fibonacci_test::case_2_one_base_case ... ok
test fibonacci_test::case_3 ... ok
test fibonacci_test::case_4 ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

§Use specific case attributes

Every function’s attributes that preceding a #[case] attribute will be used in this test case and all function’s attributes that follow the last #[case] attribute will mark all test cases.

This feature can be use to mark just some cases as should_panic and choose to have a fine grain on expected panic messages.

In follow example we run 3 tests where the first pass without any panic, in the second we catch a panic but we don’t care about the message and in the third one we also check the panic message.

use rstest::rstest;

#[rstest]
#[case::no_panic(0)]
#[should_panic]
#[case::panic(1)]
#[should_panic(expected="expected")]
#[case::panic_with_message(2)]
fn attribute_per_case(#[case] val: i32) {
    match val {
        0 => assert!(true),
        1 => panic!("No catch"),
        2 => panic!("expected"),
        _ => unreachable!(),
    }
}

Output:

running 3 tests
test attribute_per_case::case_1_no_panic ... ok
test attribute_per_case::case_3_panic_with_message ... ok
test attribute_per_case::case_2_panic ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

To mark all your tests as #[should_panic] use:

#[rstest]
#[case(1)]
#[case(2)]
#[case(3)]
#[should_panic]
fn fail(#[case] v: u32) { assert_eq!(0, v) }

§Values Lists

Another useful way to write a test and execute it for some values is to use the values list syntax. This syntax can be useful both for a plain list and for testing all combination of input arguments.


#[rstest]
fn should_be_valid(
    #[values("John", "alice", "My_Name", "Zigy_2001")]
    input: &str
) {
    assert!(is_valid(input))
}

or


#[rstest]
fn should_accept_all_corner_cases(
    #[values("J", "A", "A________________________________________21")]
    name: &str,
    #[values(14, 100)]
    age: u8
) {
    assert!(valid_user(name, age))
}

where cargo test output is

test should_accept_all_corner_cases::name_1___J__::age_2_100 ... ok
test should_accept_all_corner_cases::name_2___A__::age_1_14 ... ok
test should_accept_all_corner_cases::name_2___A__::age_2_100 ... ok
test should_accept_all_corner_cases::name_3___A________________________________________21__::age_2_100 ... ok
test should_accept_all_corner_cases::name_3___A________________________________________21__::age_1_14 ... ok
test should_accept_all_corner_cases::name_1___J__::age_1_14 ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Note that the test names contains the given expression sanitized into a valid Rust identifier name. This should help to identify which case fails.

If you prefer to provide an explicit name you can use doc comments before each value inside the #[values(...)] attribute:

#[rstest]
fn describe_values(
    #[values(
        /// short_value
        42,
        /// long_value
        100,
    )]
    value: u32,
) {
    assert!(value > 0);
}

The generated tests will be named value_1_short_value and value_2_long_value instead of using the raw expression text.

Also value list implements the magic conversion feature: every time the value type implements FromStr trait you can use a literal string to define it.

#[rstest]
fn given_port(#[values("1.2.3.4:8000", "4.3.2.1:8000", "127.0.0.1:8000")] addr: SocketAddr) {
    assert_eq!(8000, addr.port())
}

§Destructuring inputs

Both parametrized case and values can be destructured:

struct S {
    first: u32,
    second: u32,
}

struct T(i32);

#[rstest]
#[case(S{first: 21, second: 42})]
fn some_test(#[case] S{first, second} : S, #[values(T(-1), T(1))] T(t): T) {
    assert_eq!(1, t * t);
    assert_eq!(2 * first, second);
}

§Files path as input arguments

If you need to create a test for each file in a given location you can use #[files("glob path syntax")] attribute to generate a test for each file that satisfy the given glob path.

#[rstest]
fn for_each_file(#[files("src/**/*.rs")] #[exclude("test")] path: PathBuf) {
    assert!(check_file(&path))
}

The default behavior is to ignore the files that start with ".", but you can modify this by use #[include_dot_files] attribute. The files attribute can be used more than once on the same variable, and you can also create some custom exclusion rules with the #[exclude("regex")] attributes that filter out all paths that verify the regular expression.

Sometime is useful to have test files in a workspace folder to share them between the crates in your workspace. You can do that by use the usual parent folders .. in the glob path. In this case the test names will be the relative path from the crate root where the parent folder components are replaced by _UP: for instance if you have a valid_call.yaml in the folder ../test_cases (from your crate root) a test name could be path_1__UP_test_cases_valid_call_yaml.

If you want to change the base path for the test files. You can do that by using #[base_dir = "..."] attribute. The base_dir is resolved relative to the crate root (similar to the files attribute without base_dir). If you want to use an absolute path you can use #[base_dir = "/path/to/your/files"].

Both files and base_dir attributes can use environment variables using the syntax $VAR or ${VAR}. If the environment variable is not set, the attribute will cause an error. This can be ignored using the #[ignore_missing_env_vars] attribute. A default value can be provided for the environment variable using the syntax ${VAR:-default} (similar to bash). This can be really useful when you want to override the cargo root on some environments: use #[base_dir = "BASE_TEST_DIR:-"] do the trick.

The #[files(...)] attribute ignores matched directory paths by default, returning only file paths. If you need the test cases to include directories found by the glob pattern, use the #[dirs] attribute in conjunction with #[files(...)].

Finally, often you would to recompile tests sources when file the folders or the environment variables changed. In this case you should provide a build.rs script file that tell to the compiler what to look in order to recompile the tests. For instance follow a simple example:

ⓘ
pub fn main() {
    println!("cargo::rerun-if-changed=tests/resources");
    println!("cargo::rerun-if-env-changed=BASE_TEST_DIR");
}

§#[mode = ...]

Using the #[mode = ...] attribute on an argument tagged with #[files(...)] and friends will set the way the file is passed as an argument. It accepts the following arguments with the following behaviour:

  • path: the default behaviour, passes each file path as a PathBuf.
  • str: passes the contents of each file as a &str using include_str.
  • bytes: passes the contents of each files as a &[u8] using include_bytes.

Trying to pass directories as arguments will cause a compile error when using str and bytes.

#[rstest]
fn for_each_path(
    #[files("src/**/*.rs")] #[exclude("test")] #[mode = path] path: PathBuf
) {
    let contents = read_to_string(path).unwrap();
    assert!(contents.len() >= 0)
}

#[rstest]
fn for_each_txt_file(
    #[files("src/**/*.rs")] #[exclude("test")] #[mode = str] contents: &str
) {
    assert!(contents.len() >= 0)
}

#[rstest]
fn for_each_bin_file(
    #[files("src/**/*.rs")] #[exclude("test")] #[mode = bytes] contents: &[u8]
) {
    assert!(contents.len() >= 0)
}

§Use Parametrize definition in more tests

If you need to use a test list for more than one test you can use rstest_reuse crate. With this helper crate you can define a template and use it everywhere.

ⓘ
use rstest::rstest;
use rstest_reuse::{self, *};

#[template]
#[rstest]
#[case(2, 2)]
#[case(4/2, 2)]
fn two_simple_cases(#[case] a: u32, #[case] b: u32) {}

#[apply(two_simple_cases)]
fn it_works(#[case] a: u32,#[case] b: u32) {
    assert_eq!(a, b);
}

See rstest_reuse for more details.

§Async

rstest supports async tests, but makes no assumptions about what async runtime you are using. Async tests require either an explicit test attribute or an implicit test attribute.

You are responsible for providing any appropriate peer dependencies to run your async tests.

If your test input is an async value (fixture or test parameter) you can use #[future] attribute to remove impl Future<Output = T> boilerplate and just use T:

use rstest::*;
#[fixture]
async fn base() -> u32 { 42 }

#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
#[tokio::test]
async fn my_async_test(#[future] base: u32, #[case] expected: u32, #[future] #[case] div: u32) {
    assert_eq!(expected, base.await / div.await);
}

As you noted you should .await all future values and this some times can be really boring. In this case you can use #[future(awt)] to awaiting an input or annotating your function with #[awt] attributes to globally .await all your future inputs. Previous code can be simplified like follow:

use rstest::*;

#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
#[tokio::test]
#[awt]
async fn global(#[future] base: u32, #[case] expected: u32, #[future] #[case] div: u32) {
    assert_eq!(expected, base / div);
}

#[rstest]
#[case(21, async { 2 })]
#[case(6, async { 7 })]
#[tokio::test]
async fn single(#[future] base: u32, #[case] expected: u32, #[future(awt)] #[case] div: u32) {
    assert_eq!(expected, base.await / div);
}

§Test #[timeout()]

You can define an execution timeout for your tests with #[timeout(<duration>)] attribute. Timeout works both for sync and async tests and is runtime agnostic. #[timeout(<duration>)] take an expression that should return a std::time::Duration. Follow a simple async example:

use rstest::*;
use std::time::Duration;

async fn delayed_sum(a: u32, b: u32,delay: Duration) -> u32 {
    async_std::task::sleep(delay).await;
    a + b
}

#[rstest]
#[timeout(Duration::from_millis(80))]
#[async_std::test]
async fn single_pass() {
    assert_eq!(4, delayed_sum(2, 2, ms(10)).await);
}

In this case test pass because the delay is just 10 milliseconds and timeout is 80 milliseconds.

You can use timeout attribute like any other attribute in your tests, and you can override a group timeout with a test specific one. In the follow example we have 3 tests where first and third use 100 milliseconds but the second one use 10 milliseconds. Another valuable point in this example is to use an expression to compute the duration.

fn ms(ms: u32) -> Duration {
    Duration::from_millis(ms.into())
}

#[rstest]
#[case::pass(ms(1), 4)]
#[timeout(ms(10))]
#[case::fail_timeout(ms(60), 4)]
#[case::fail_value(ms(1), 5)]
#[timeout(ms(100))]
#[async_std::test]
async fn group_one_timeout_override(#[case] delay: Duration, #[case] expected: u32) {
    assert_eq!(expected, delayed_sum(2, 2, delay).await);
}

If you want to use timeout for async test you need to use async-timeout feature (enabled by default).

§Default timeout

You can set a default timeout for test using the RSTEST_TIMEOUT environment variable. The value is in seconds and is evaluated on test compile time.

§Test Attributes

§Explicit Test Attributes

If your test contains an attribute #[test_attr(...)], this is an explicit test attribute declaration. The contents of the parentheses will be inserted as an attribute of your test. This allows for arbitrary test attribute syntax.

For instance, if you want to test an async function with smol_macros::test, you can write

use rstest::*;
use macro_rules_attribute::apply;
use smol_macros::test;
use std::future::Future;

#[rstest]
#[case(2, async { 4 })]
#[case(21, async { 42 })]
#[test_attr(apply(test))]
async fn my_async_test(#[case] a: u32, #[case] #[future] result: u32) {
    assert_eq!(2 * a, result.await);
}

§Implicit Test Attributes

If your test contains an attribute whose path ends in test, this is treated as an implicit test attribute, and replaces the default #[test] attribute. Simply including the attribute in your test’s attribute list is all that is required.

For instance, if you want to test an async function with the actix_rt::test attribute, you can just write:

use rstest::*;
use actix_rt;
use std::future::Future;

#[rstest]
#[case(2, async { 4 })]
#[case(21, async { 42 })]
#[actix_rt::test]
async fn my_async_test(#[case] a: u32, #[case] #[future] result: u32) {
    assert_eq!(2 * a, result.await);
}

Implicit test attributes have a lower priority than explicit test attributes. If you have an unrelated attribute whose path ends in test, you should declare an explicit test attribute for deconfliction.

§Default Test Attributes

Synchronous tests use #[test] as the default test attribute if nothing else is supplied explicitly or implicitly.

Asynchronous tests will fail to compile if no test attribute is specified explicitly or implicitly.

§Test Context object

You can have a Context object for your test by annotating an argument by #[context] attribute. This object contains some useful information both to implement simple logics and debugging stuff.

use rstest::{rstest, Context};

#[rstest]
#[case::a_description(42)]
fn my_test(#[context] ctx: Context, #[case] _c: u32) {
    assert_eq!("my_test", ctx.name);
    assert_eq!(Some("a_description"), ctx.description);
    assert_eq!(Some(0), ctx.case);

    std::thread::sleep(std::time::Duration::from_millis(100));
    assert!(ctx.start.elapsed() >= std::time::Duration::from_millis(100));
}

§Local lifetime and #[by_ref] attribute

In some cases you may want to use a local lifetime for some arguments of your test. In these cases you can use the #[by_ref] attribute then use the reference instead the value.

enum E<'a> {
    A(bool),
    B(&'a Cell<E<'a>>),
}

fn make_e_from_bool<'a>(_bump: &'a (), b: bool) -> E<'a> {
    E::A(b)
}

#[fixture]
fn bump() -> () {}

#[rstest]
#[case(true, E::A(true))]
fn it_works<'a>(#[by_ref] bump: &'a (), #[case] b: bool, #[case] expected: E<'a>) {
    let actual = make_e_from_bool(&bump, b);
    assert_eq!(actual, expected);
}

You can use #[by_ref] attribute for all arguments of your test and not just for fixture but also for cases, values and files.

§Putting all Together

All these features can be used together with a mixture of fixture variables, fixed cases and bunch of values. For instance, you might need two test cases which test for panics, one for a logged in user and one for a guest user.


use rstest::*;

#[fixture]
fn repository() -> InMemoryRepository {
    let mut r = InMemoryRepository::default();
    // fill repository with some data
    r
}

#[fixture]
fn alice() -> User {
    User::logged("Alice", "2001-10-04", "London", "UK")
}

#[rstest]
#[case::authorized_user(alice())] // We can use `fixture` also as standard function
#[case::guest(User::Guest)]   // We can give a name to every case : `guest` in this case
#[should_panic(expected = "Invalid query error")] // We would test a panic
fn should_be_invalid_query_error(
    repository: impl Repository,
    #[case] user: User,
    #[values("     ", "^%$some#@invalid!chars", ".n.o.d.o.t.s.")] query: &str,
    query: &str
) {
    repository.find_items(&user, query).unwrap();
}

§Ignoring Arguments

Sometimes, you may want to inject and use fixtures not managed by rstest (e.g. db connection pools for sqlx tests).

In these cases, you can use the #[ignore] attribute to ignore the additional parameter and let another crate take care of it:

ⓘ
use rstest::*;
use sqlx::*;

#[fixture]
fn my_fixture() -> i32 { 42 }

#[rstest]
#[sqlx::test]
async fn test_db(my_fixture: i32, #[ignore] pool: PgPool) {
    assert_eq!(42, injected);
    // do stuff with the connection pool
}

§Trace Input Arguments

Sometimes can be very helpful to print all test’s input arguments. To do it you can use the #[trace] function attribute that you can apply to all cases or just to some of them.

use rstest::*;

#[fixture]
fn injected() -> i32 { 42 }

#[rstest]
#[trace]
fn the_test(injected: i32) {
    assert_eq!(42, injected)
}

Will print an output like

Testing started at 14.12 ...
------------ TEST ARGUMENTS ------------
injected = 42
-------------- TEST START --------------


Expected :42
Actual   :43

But

#[rstest]
#[case(1)]
#[trace]
#[case(2)]
fn the_test(#[case] v: i32) {
    assert_eq!(0, v)
}

will trace just case_2 input arguments.

If you want to trace input arguments but skip some of them that don’t implement the Debug trait, you can also use the #[notrace] argument attribute to skip them:

#[rstest]
#[trace]
fn the_test(injected: i32, #[notrace] xyz: Xyz, #[notrace] have_no_sense: NoSense) {
    assert_eq!(42, injected)
}

§Old compact syntax

rstest support also a syntax where all options and configuration can be write as rstest attribute arguments. This syntax is a little less verbose but make composition harder: for instance try to add some cases to a rstest_reuse template is really hard.

So we’ll continue to maintain the old syntax for a long time but we strongly encourage to switch your test in the new form.

Anyway, here we recall this syntax and rewrite the previous example in the compact form.

rstest(
    arg_1,
    ...,
    arg_n[,]
    [::attribute_1[:: ... [::attribute_k]]]
)

Where:

  • arg_i could be one of the follow
    • ident that match to one of function arguments for parametrized cases
    • case[::description](v1, ..., vl) a test case
    • fixture(v1, ..., vl) [as argument_name] where fixture is the injected fixture and argument_name (default use fixture) is one of function arguments that and v1, ..., vl is a partial list of fixture’s arguments
    • ident => [v1, ..., vl] where ident is one of function arguments and v1, ..., vl is a list of values for ident
  • attribute_j a test attribute like trace or notrace

§Fixture Arguments

#[rstest(user("Bob"))]
fn check_user(user: User) {
    assert_eq("Bob", user.name())
}

§Fixture Rename

#[fixture]
fn long_and_boring_descriptive_name() -> i32 { 42 }

#[rstest(long_and_boring_descriptive_name as short)]
fn the_test(short: i32) {
    assert_eq!(42, short)
}

§Parametrized

#[rstest(input, expected,
    case::zero_base_case(0, 0),
    case::one_base_case(1, 1),
    case(2, 1),
    case(3, 2),
    #[should_panic]
    case(4, 42)
)]
fn fibonacci_test(input: u32, expected: u32) {
    assert_eq!(expected, fibonacci(input))
}

§Values Lists


#[rstest(
    input => ["John", "alice", "My_Name", "Zigy_2001"]
)]
fn should_be_valid(input: &str) {
    assert!(is_valid(input))
}

§trace and notrace

#[rstest(::trace::notrace(xzy, have_no_sense))]
fn the_test(injected: i32, xyz: Xyz, have_no_sense: NoSense) {
    assert_eq!(42, injected)
}