#[rstest]
Expand description
The attribute that you should use for your tests. Your
annotated function’s arguments can be
injected with
[fixture]
s, 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
If the test function is an async
function rstest
will run all tests as async
tests. You can use it just with async-std
and you should include attributes
in
async-std
’s features.
In your test function you can:
- injecting fixtures
- Generate parametrized test cases
- Generate tests for each combination of value lists
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 dependecy 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)
}
Sometimes is useful to have some parametes 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 indipendent tests and not just one that
check every case. Every test can fail indipendently 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 expresions 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);
}
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_commedy", 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))
}
Outuput 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 usefull both for a plain list and for testing all combination of input arguments.
#[rstest]
fn should_be_valid(
#[values("Jhon", "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 wich case fails.
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.
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.
#[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())
}
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!(a == b);
}
See rstest_reuse
for more dettails.
Async
rstest
provides out of the box async
support. Just mark your
test function as async
and it’ll use #[async-std::test]
to
annotate it. This feature can be really useful to build async
parametric tests using a tidy syntax:
use rstest::*;
#[rstest]
#[case(5, 2, 3)]
#[should_panic]
#[case(42, 40, 1)]
async fn my_async_test(#[case] expected: u32, #[case] a: u32, #[case] b: u32) {
assert_eq!(expected, async_sum(a, b).await);
}
Currently only async-std
is supported out of the box. But if you need to use
another runtime that provide it’s own test attribute (i.e. tokio::test
or
actix_rt::test
) you can use it in your async
test like described in
Inject Test Attribute.
To use this feature, you need to enable attributes
in the async-std
features list in your Cargo.toml
:
async-std = { version = "1.5", features = ["attributes"] }
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 })]
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 #[timeout(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 })]
#[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 })]
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. Timeouts
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 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 attibute 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 millis but the second one use 10 millis.
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 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).
Inject Test Attribute
If you would like to use another test
attribute for your test you can simply
indicate it in your test function’s attributes. For instance if you want
to test some async function with use 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);
}
Just the attributes that ends with test
(last path segment) can be injected:
in this case the #[actix_rt::test]
attribute will replace the standard #[test]
attribute.
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::authed_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 whould 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();
}
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 istance 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 followident
that match to one of function arguments for parametrized casescase[::description](v1, ..., vl)
a test casefixture(v1, ..., vl) [as argument_name]
where fixture is the injected fixture and argument_name (default use fixture) is one of function arguments that andv1, ..., vl
is a partial list of fixture’s argumentsident => [v1, ..., vl]
whereident
is one of function arguments andv1, ..., vl
is a list of values for ident
attribute_j
a test attribute liketrace
ornotrace
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 => ["Jhon", "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)
}