CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/rstest-tests

Configures and runs rstest - Rust parametrized + fixture-based testing crate; `#[rstest]` attribute + `#[case(...)]` for parametrize; `#[fixture]` for reusable test setup; matrix tests via multiple `#[case]` × N (cartesian product); async test support via `#[async_std::test]` / `#[tokio::test]` + `#[rstest]`; `#[future]` for async fixtures. Use when working with Rust and needing parametrize/fixture patterns beyond stdlib `#[test]`.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files
name:
rstest-tests
description:
Configures and runs rstest - Rust parametrized + fixture-based testing crate; `#[rstest]` attribute + `#[case(...)]` for parametrize; `#[fixture]` for reusable test setup; matrix tests via multiple `#[case]` × N (cartesian product); async test support via `#[async_std::test]` / `#[tokio::test]` + `#[rstest]`; `#[future]` for async fixtures. Use when working with Rust and needing parametrize/fixture patterns beyond stdlib `#[test]`.

rstest-tests

Overview

Per github.com/la10736/rstest:

rstest is a Rust crate for parametrize + fixture testing - features that Rust's stdlib #[test] lacks. Pairs with cargo-test (rstest tests are still discovered by cargo test).

When to use

  • Same input pattern repeated across many tests (parametrize wins).
  • Setup code shared across multiple tests (fixture wins over mod tests shared state).
  • Async test patterns benefit from rstest's async-fixture support.
  • Migration from pytest/JUnit5 patterns to Rust.

Step 1 - Install

Cargo.toml:

[dev-dependencies]
rstest = "0.21"

Step 2 - Parametrize with #[case]

use rstest::rstest;

#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[case(-1, 1, 0)]
#[case(100, 200, 300)]
fn add_cases(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
    assert_eq!(add(a, b), expected);
}

Each #[case] runs as a separate test - failures don't stop subsequent cases. Test names become add_cases::case_1, add_cases::case_2, etc.

Step 3 - Named cases

#[rstest]
#[case::positive(1, 2, 3)]
#[case::zero(0, 0, 0)]
#[case::negative(-1, 1, 0)]
fn add_named(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
    assert_eq!(add(a, b), expected);
}

Test names: add_named::positive, add_named::zero, etc. - much better for failure logs.

Step 4 - Fixtures

use rstest::{fixture, rstest};

#[fixture]
fn db() -> Database {
    Database::new_test_instance()
}

#[fixture]
fn user(db: Database) -> User {
    db.create_user("alice")
}

#[rstest]
fn test_user_id(user: User) {
    assert_eq!(user.id, 1);
}

#[rstest]
fn test_user_email(user: User, db: Database) {
    assert_eq!(db.find_user(user.id).email, "alice@example.com");
}

Fixtures are functions with #[fixture]; tests with #[rstest] auto-receive them via parameter name matching. Fixture chaining works (a fixture can request other fixtures).

Step 5 - Matrix tests (cartesian product)

#[rstest]
fn test_matrix(
    #[values("alice", "bob", "charlie")] name: &str,
    #[values(0, 18, 65)] age: u32,
) {
    let user = User::new(name, age);
    assert!(user.is_valid());
}

Runs 3 × 3 = 9 tests with all combinations. Equivalent to nested #[case] for cartesian product.

Step 6 - Combined #[case] + #[values]

#[rstest]
#[case::happy("alice")]
#[case::edge("a")]
fn test_with_values(
    #[case] base_name: &str,
    #[values(0, 1, 100)] count: u32,
) {
    // happy × 3 values = 3 tests
    // edge × 3 values = 3 tests
}

Step 7 - Async tests

use rstest::rstest;

#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[tokio::test]
async fn async_add_cases(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
    let result = add_async(a, b).await;
    assert_eq!(result, expected);
}

For async fixtures, use #[future]:

use rstest::*;

#[fixture]
async fn db_async() -> Database {
    Database::connect_async().await.unwrap()
}

#[rstest]
#[tokio::test]
async fn test_async(#[future] db_async: Database) {
    let db = db_async.await;
    assert!(db.is_connected());
}

Step 8 - Indirect fixtures (parameterize fixture instances)

#[fixture]
fn user(#[default("alice")] name: &str) -> User {
    User::new(name)
}

#[rstest]
#[case::alice("alice")]
#[case::bob("bob")]
fn test_user(#[case] expected_name: &str, #[with(expected_name)] user: User) {
    assert_eq!(user.name, expected_name);
}

#[with(...)] injects custom args into the fixture for that test.

Step 9 - CI integration

Same as plain cargo test:

- run: cargo test --all-targets

rstest tests are discovered + run by cargo test natively - no separate runner.

Anti-patterns

Anti-patternWhy it failsFix
Use rstest for single-case testsAdds dependency for no benefitPlain #[test] for single cases
Skip #[case::name]Test names become case_1, case_2; debug-hostileAlways name cases (Step 3)
Heavy matrix (5 dims × 5 values = 3125 tests)Combinatorial explosionStrategic cases vs full matrix
Mix sync + async tests in same parametrizeConfusingSeparate #[rstest] blocks per sync/async

Limitations

  • Adds a dev dependency for what's syntactic sugar over #[test].
  • Test name generation can be opaque without explicit case names.
  • Some IDE integrations don't show parametrized test cases as separate items.
  • Async fixture support is solid but newer; pin recent rstest version.

References

  • rs-gh - rstest repository + docs
  • crates.io/crates/rstest - published crate
  • docs.rs/rstest - API reference
  • go-test, ginkgo-tests, cargo-test - sister tools
  • proptest-testing (qa-property-based) - Rust property-based
  • test-code-conventions (qa-test-review)
Workspace
testland
Visibility
Public
Created
Last updated
Publish Source
GitHub
Badge
testland/rstest-tests badge