Framework
la10736/rstest avatar
la10736/rstest

rstest for Rust: fixtures, parameterized tests, and file inputs

Fixture-based test framework for Rust

1,598 stars77 forksRustApache-2.0

At a glance

What is it?
rstest is a procedural-macro test framework for Rust projects that need reusable fixtures, table-driven cases, generated file tests, or async inputs. It integrates with Cargo tests but leaves async runtime selection to the project.
Who is it for?
Adopt rstest when a Rust test suite benefits from injected fixtures, explicit case tables, reusable templates, or file-derived inputs. Do not choose it merely to avoid async runtime setup or as a replacement for a property-testing framework.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 20 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • la10736/rstest1,598 ★ · +5
Star history
Repository2026-10-172026-11-07
la10736/rstest1.6k1.6k

5 Stars in the last 21 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Why rstest fits Rust dependency injection and case tables

rstest addresses a specific problem in Rust testing: many tests need setup values, but duplicating that setup makes cases harder to read and maintain. Its central mechanism is argument injection. A function marked with `#[fixture]` becomes a reusable source of test input, while a test marked with `#[rstest]` accepts that value as an ordinary function argument. The fixture returns `u32` and both tests receive `fixture: u32`, so the setup contract is visible in the test signature.

The README also positions rstest as a table-based testing framework. Repeated `#[case(...)]` attributes produce independent tests rather than a loop inside one test body. This matters for reporting because Cargo's test output can identify each generated case separately. The Fibonacci example declares five cases and the documentation shows five passing test entries, including names such as `fibonacci_test::case_1`. A failure can therefore be tied to a particular row in the case table instead of reporting one aggregate failure.

The intended audience is Rust developers writing unit or integration tests whose inputs vary across dimensions, files, or asynchronous computations. rstest is most useful when test setup has a stable shape that can be expressed through function arguments and macro attributes. It is not intended to replace assertions, mocking libraries, fuzzing tools, or the Cargo test harness itself. The value comes from generating test functions and wiring inputs, not from defining a different assertion model.

How rstest expands fixtures, values, and cases

The macro layer supplies several related forms of test expansion. `#[case]` takes explicit arguments, which works well when related values must remain grouped. In the documented `fibonacci_test`, input and expected output appear on the same case declaration. `#[values(list, of, values)]` is different: it accepts a collection of values for one argument and creates a case for each value. This suits tests that vary one input without requiring a separate expected value for every row.

When lists are supplied for several arguments, rstest creates their cartesian product. That behavior is useful for bounded combinations, such as testing several formats against several parser modes. It can also expand test count quickly. The project does not document a way to cap that expansion, so users should account for combinatorics when choosing list sizes. The README gives `#[files]` a similar expansion role for paths, but file matching occurs during compilation rather than through runtime iteration.

rstest also documents magic conversion for types implementing `FromStr`. A case can contain a string such as `"1.2.3.4:8080"`, and the `SocketAddr` parameter is parsed from that literal. The documentation says the same mechanism applies to value lists and fixture default values. This avoids writing parser boilerplate in every case, although it also means the case declaration must match the target type's `FromStr` semantics. A malformed literal becomes a conversion error during test construction or execution, depending on the generated code path.

Feature-gated cases use `#[cfg_attr(feature = "frac", case(4/2, 2))]`. That keeps optional inputs aligned with Cargo feature configuration. The result is conditional test generation rather than a runtime `if feature` inside the test body. This is a compact option for configurations that should have distinct case tables, but it also means the effective suite changes with enabled features.

Installing rstest and running the first parameterized test

The README instructs users to add rstest as a development dependency in `Cargo.toml`. The documented version is `0.27.0`, which matches the most recent release listed for the repository.

toml
[dev-dependencies]
rstest = "0.27.0"

After Cargo resolves the dependency, a parameterized test can be added as normal Rust test code. The following is the README's five-case Fibonacci example, including the `rstest` import.

rust
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))
}

Run the suite with the command documented by the project:

bash
cargo test

Cargo should list one generated test for each case, with names based on the test function and case number. The README shows five passing entries for this example. The function `fibonacci` is assumed to exist in the surrounding test module. If it is not present, the example will not compile until the application or test helper defines it.

The default feature list includes `async-timeout` and `crate-name`. The README identifies `async-timeout` as the timeout support for async tests and `crate-name` as support for importing the rstest package under a different name. The initial dependency line enables the crate's defaults, so this basic example does not require a feature declaration.

Async tests leave runtime selection to the Rust project

rstest supports asynchronous fixtures and test parameters, but it does not select an async runtime for the project. The README explicitly says that the user is responsible for providing the appropriate peer dependencies. An async test therefore needs both rstest and a runtime-specific test attribute, such as the example's `#[tokio::test]`.

The `#[future]` attribute removes the need to expose `impl Future<Output = T>` as the test parameter type. A fixture may return a future, and `#[future] base: u32` lets the test refer to the eventual output after awaiting it. The example still requires explicit `.await` calls on the fixture and case values.

rust
#[rstest]
#[tokio::test]
#[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);
}

For repetitive awaiting, the README documents two alternatives. `#[future(awt)]` awaits one input at the parameter, while `#[awt]` on the function globally awaits future inputs. The documented spelling `awt` is a project API name and should not be corrected to `await`. This feature reduces repeated `.await` syntax, but it also changes where waiting is applied, so test authors should keep runtime ownership clear.

The `async-timeout` feature is enabled by default and is described as providing `timeout` for async tests. An adoption decision should verify the exact timeout attribute syntax in the full API documentation before relying on it. The main limitation is architectural: rstest composes with a chosen runtime, while the runtime itself remains outside the framework.

Compile-time file discovery creates one test per matched path

rstest can treat filesystem paths as generated test inputs through `#[files("glob path syntax")]`. The documented pattern `src/**/*.rs` searches under `src` for Rust files. Directories are excluded by default, and files whose names begin with `.` are ignored unless `#[include_dot_files]` is present. Repeated `files` attributes can contribute more patterns, and `#[exclude("regex")]` removes paths matching a regular expression.

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

Path resolution happens at compile time relative to `CARGO_MANIFEST_DIR`. That is a meaningful boundary. Files created after compilation do not automatically become new cases until the test target is compiled again. The `#[base_dir = "..."]` attribute changes the root, but the selected directory must already exist. Relative paths used for generated test names are also resolved from that root.

Environment variables can appear in paths as `${ENV_VAR_NAME}`. The README also documents `${ENV_VAR_NAME:-default_value}` for a default. This can select different fixture trees without changing the glob expression. Since resolution occurs at compile time, changing the environment can require recompilation before Cargo reports the revised case set.

The `#[dirs]` attribute allows directories matched by the pattern to be included alongside files. For linting or format checks, this creates one test per discovered item and lets existing assertion code decide whether it passes. The trade-off is compile-time filesystem coupling. A moving development environment can produce a different number of tests even when the Rust source has not changed.

Maintenance, licensing, and the boundary around rstest_reuse

The repository is not archived, and its last push was on 2026-09-06. The release list also places `v0.27.0` on 2026-09-06, following `v0.26.1` from 2025-07-27. This supports treating recent development as a current fact, but the README excerpt does not document a support policy for older Rust toolchains or a long-term compatibility guarantee. Projects should therefore check the crate metadata and release notes for the compiler versions relevant to their build.

The repository description identifies `Apache-2.0`, and the repository contains both `LICENSE-APACHE` and `LICENSE-MIT`. The README also carries badges for Apache 2.0 and MIT licensing. That dual-license presentation should be checked against the repository's license files when integrating rstest into a distribution. This is a compliance check, not legal advice.

Cross-test case reuse belongs to a separate crate, `rstest_reuse`, not to the core example set. Its documented `#[template]` creates a reusable parameterized definition, while `#[apply(two_simple_cases)]` applies that template to a concrete test. This separation keeps the core dependency focused on fixtures and case generation, but it also means teams wanting shared case tables must add and manage another crate.

rstest is the wrong choice when a suite needs runtime discovery of files that are guaranteed to change independently of compilation, or when no procedural-macro-based expansion fits the test structure. A plain `cargo test` suite remains simpler for fixed unit tests. The framework earns its dependency when recurring setup, bounded case matrices, or compile-time path discovery would otherwise produce repetitive code.

Editorial conclusion

Adopt rstest when a Rust test suite benefits from injected fixtures, explicit case tables, reusable templates, or file-derived inputs. Do not choose it merely to avoid async runtime setup or as a replacement for a property-testing framework. Before adoption, verify that required features and async runtime dependencies are configured correctly, then confirm that generated test names remain useful in failure output.

Frequently asked questions

How do I add rstest to a Rust project?

Add `rstest = "0.27.0"` under `[dev-dependencies]` in `Cargo.toml`, then run tests through Cargo.

Does rstest choose an async runtime for Rust tests?

No. The README states that the project must provide the appropriate async runtime dependencies and use an explicit test attribute, such as the example's `#[tokio::test]`.

How does rstest create multiple tests from inputs?

Each `#[case(...)]` declaration generates an independent test. `#[values(...)]` creates tests for listed values, while lists attached to multiple arguments create their cartesian product.

Can rstest generate tests from files?

Yes. `#[files("glob path syntax")]` creates a case for each matching file, with compile-time path resolution relative to the Cargo project root by default.

Can rstest reuse parameterized cases across tests?

The README directs users to the separate `rstest_reuse` crate for defining a template and applying it to multiple tests.

Official sources

  1. Issues
  2. la10736/rstest on GitHub
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/la10736-rstest.svg)](https://hysenlabs.com/projects/la10736-rstest)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/la10736-rstest.svg)](https://hysenlabs.com/projects/la10736-rstest)

Was this useful?