# Mastering test fixtures in Rust with `rstest`

Testing is an indispensable part of modern software development, ensuring code
reliability, maintainability, and correctness. In the Rust ecosystem, while the
built-in testing framework provides a solid foundation, managing test
dependencies, and creating parameterized tests can become verbose. The `rstest`
crate (<https://github.com/la10736/rstest>) emerges as a powerful solution,
offering a sophisticated fixture-based, parameterized testing framework that
significantly simplifies these tasks through the use of procedural macros. This
document provides a comprehensive exploration of `rstest`, from fundamental
concepts to advanced techniques, enabling Rust developers to write cleaner,
more expressive, and robust tests.

## 1. Introduction to `rstest` and test fixtures in Rust

### A. What are test fixtures and why use them?

In software testing, a **test fixture** refers to a fixed state of a set of
objects used as a baseline for running tests. The primary purpose of a fixture
is to ensure that a well-known, controlled environment exists before tests run,
so the results remain repeatable. Test dependencies, such as database
connections, user objects, or specific configurations, often require careful
setup before a test executes and, sometimes, teardown afterward. Managing this
setup, together with the teardown logic, within each test function can lead to
considerable boilerplate and repetition, making tests harder to read, maintain,
and extend.

Fixtures address this by encapsulating these dependencies and their setup
logic. For instance, if multiple tests require a logged-in user object or a
pre-populated database, instead of creating these in every test, a fixture can
provide them. This approach allows developers to focus on the specific logic
being tested rather than the auxiliary utilities.

Fundamentally, the use of fixtures promotes a crucial separation of concerns:
the *preparation* of the test environment is decoupled from the *execution* of
the test logic. Traditional testing approaches often intermingle setup, action,
and assertion logic within a single test function. This can result in lengthy,
and convoluted, tests that are difficult to comprehend at a glance. By
extracting the setup logic into reusable components (fixtures), the actual test
functions become shorter, more focused, and thus more readable, maintainable,
and trustworthy.

### B. Introducing `rstest`: simplifying fixture-based testing in Rust

`rstest` is a Rust crate specifically designed to simplify and enhance testing
by leveraging the concept of fixtures and providing powerful parameterization
capabilities. It is available on `crates.io` and its source code is hosted at
<https://github.com/la10736/rstest>, distinguishing it from other software
projects that may share the same name but operate in different ecosystems
(e.g., a JavaScript/TypeScript framework mentioned).

The `rstest` crate utilizes Rust's procedural macros, such as `#[rstest]` and
`#[fixture]`, to achieve its declarative and expressive syntax. These macros
allow developers to define fixtures and inject them into test functions simply
by listing them as arguments. This compile-time mechanism inspects test
function signatures and fixture definitions to wire up dependencies
automatically.

This reliance on procedural macros is a key architectural decision. It enables
`rstest` to offer a remarkably clean and intuitive syntax at the test-writing
level. Developers declare the dependencies their tests need, and the macros
handle dependency resolution as well as injection. While this significantly
improves the developer experience for writing tests, the underlying macro
expansion involves compile-time code generation. This complexity, though
hidden, can have implications for build times, particularly in large test
suites. Furthermore, understanding the macro expansion can sometimes be
necessary for debugging complex test scenarios or unexpected behaviour.

### C. Core benefits: readability, reusability, reduced boilerplate

The primary advantages of using `rstest` revolve around enhancing test code
quality and developer productivity:

- **Readability:** By injecting dependencies as function arguments, `rstest`
  makes the requirements of a test explicit and easy to understand. The test
  function's signature clearly documents what it needs to run. This allows
  developers to focus on the important parts of tests by abstracting away the
  setup details.
- **Reusability:** Fixtures defined with `rstest` are reusable components. A
  single fixture, such as one setting up a database connection or creating a
  complex data structure, can be used across multiple tests, eliminating
  redundant setup code.
- **Reduced Boilerplate:** `rstest` significantly cuts down on repetitive setup
  and teardown code. Parameterization features, like `#[case]` and `#[values]`,
  further reduce boilerplate by allowing the generation of multiple test
  variations from a single function.

The declarative nature of `rstest` is central to these benefits. Instead of
imperatively writing setup code within each test (the *how*), developers
declare the fixtures they need (the *what*) in the test function's signature.
This shifts the cognitive load from managing setup details in every test to
designing a system of well-defined, reusable fixtures. Over time, particularly
in larger projects, this can lead to a more robust, maintainable, and
understandable test suite as common setup patterns are centralized and managed
effectively.

## II. Getting started with `rstest`

Embarking on `rstest` usage involves a straightforward setup process, from
adding it to the project dependencies to defining and using basic fixtures.

### A. Installation and project setup (`Cargo.toml`)

To begin using `rstest`, it must be added as a development dependency in the
project's `Cargo.toml` file. This ensures that `rstest` is only compiled and
linked when running tests rather than when building the main application or
library.

Add the following lines to the project's `Cargo.toml` under the
`[dev-dependencies]` section:

```toml
[dev-dependencies]
rstest = "0.26.1" # Or the latest version available on crates.io
# rstest_macros may also be needed explicitly depending on usage or version
# rstest_macros = "0.26.1" # Check crates.io for the latest version
```

It is advisable to check `crates.io` for the latest stable version of `rstest`
(and `rstest_macros` if required separately by the version of `rstest` being
used). Using `dev-dependencies` is a standard practice in Rust for testing
libraries. This convention prevents testing utilities from being included in
production binaries, which helps keep them small while reducing compile times
for non-test builds.

When leveraging Tokio's test utilities—for example `tokio::time::pause` or the
Input/output helpers in `tokio-test`—enable the `test-util` feature via a
dev-only dependency:

```toml
[dev-dependencies]
tokio = { version = "1", default-features = false, features = ["test-util"] }
rstest = "0.26.1"
```

### B. First fixture: defining with `#[fixture]`

A fixture in `rstest` is essentially a Rust function that provides some data or
performs some setup action, with its result being injectable into tests. To
designate a function as a fixture, it is annotated with the `#[fixture]`
attribute.

Consider a simple fixture that provides a numeric value:

```rust,no_run
use rstest::fixture; // Or use rstest::*;

#[fixture]
pub fn answer_to_life() -> u32 {
    42
}
```

In this example, `answer_to_life` is a public function marked with
`#[fixture]`. It takes no arguments and returns a `u32` value of 42. The
`#[fixture]` macro effectively registers this function with the `rstest`
system, transforming it into a component that `rstest` can discover and
utilize. The return type of the fixture function (here, `u32`) defines the type
of the data that will be injected into tests requesting this fixture. Fixtures
can return any valid Rust type, from simple primitives to complex structs or
trait objects. Fixtures can also depend on other fixtures, allowing for
compositional setup.

### C. Injecting fixtures into tests with `#[rstest]`

Once a fixture is defined, it can be used in a test function. Test functions
that utilize `rstest` features, including fixture injection, must be annotated
with the `#[rstest]` attribute. The fixture is then injected by simply
declaring an argument in the test function with the same name as the fixture
function.

Here’s how to use the `answer_to_life` fixture in a test:

```rust,no_run
use rstest::{fixture, rstest}; // Or use rstest::*;

#[fixture]
pub fn answer_to_life() -> u32 {
    42
}

#[rstest]
fn test_with_fixture(answer_to_life: u32) {
    assert_eq!(answer_to_life, 42);
}
```

In `test_with_fixture`, the argument `answer_to_life: u32` signals to `rstest`
that the `answer_to_life` fixture should be injected. `rstest` resolves this by
name: it looks for a fixture function named `answer_to_life`, calls it, and
passes its return value as the argument to the test function.

The argument name in the test function serves as the primary key for fixture
resolution. This convention makes usage intuitive but necessitates careful
naming of fixtures to avoid ambiguity, especially if multiple fixtures with the
same name exist in different modules but are brought into the same scope.
`rstest` generally follows Rust's standard name resolution rules, meaning an
identically named fixture can be used in different contexts depending on
visibility and `use` declarations.

## III. Mastering fixture injection and basic usage

Understanding how fixtures behave, and how they can be structured, is key to
leveraging `rstest` effectively.

### A. Simple fixture examples

The flexibility of `rstest` fixtures allows them to provide a wide array of
data types and perform various setup tasks. Fixtures are not limited by the
kind of data they can return; any valid Rust type is permissible. This enables
fixtures to encapsulate diverse setup logic, providing ready-to-use
dependencies for tests.

Here are a few examples illustrating different kinds of fixtures:

- **Fixture returning a primitive data type:**

```rust,no_run
use rstest::*;

#[fixture]
fn default_username() -> String {
    "test_user".to_string()
}

#[rstest]
fn test_username_length(default_username: String) {
    assert!(default_username.len() > 0);
}

```

- **Fixture returning a struct:**

```rust,no_run
use rstest::*;

struct User {
    id: u32,
    name: String,
}

#[fixture]
fn sample_user() -> User {
    User {
        id: 1,
        name: "Alice".to_string(),
    }
}

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

```

- **Fixture performing setup and returning a resource (e.g., a mock
  repository):**

```rust,no_run
use rstest::*;
use std::collections::HashMap;

trait Repository {
    fn add_item(&mut self, id: &str, name: &str);
    fn get_item_name(&self, id: &str) -> Option<String>;
}

#[derive(Default)]
struct MockRepository {
    data: HashMap<String, String>,
}

impl Repository for MockRepository {
    fn add_item(&mut self, id: &str, name: &str) {
        self.data.insert(id.to_string(), name.to_string());
    }

    fn get_item_name(&self, id: &str) -> Option<String> {
        self.data.get(id).cloned()
    }
}

#[fixture]
fn empty_repository() -> impl Repository {
    MockRepository::default()
}

#[rstest]
fn test_add_to_repository(mut empty_repository: impl Repository) {
    empty_repository.add_item("item1", "Test Item");
    assert_eq!(
        empty_repository.get_item_name("item1"),
        Some("Test Item".to_string())
    );
}

```

This example demonstrates a fixture providing a mutable `Repository`
implementation.

### B. Understanding fixture scope and lifetime (default behaviour)

By default, `rstest` calls a fixture function anew for each test that uses it.
This means if five different tests inject the same fixture, the fixture
function will be executed five times, and each test will receive a fresh,
independent instance of the fixture's result. This behaviour is crucial for
test isolation. The `rstest` macro effectively desugars a test like
`fn the_test(injected: i32)` into something conceptually similar to
`#[test] fn the_test() { let injected = injected_fixture_func(); /* … */ }`
within the test body, implying a new call each time.

Test isolation prevents the state from one test from inadvertently affecting
another. If fixtures were shared by default, a mutation to a fixture's state in
one test could lead to unpredictable behaviour or failures in subsequent tests
that use the same fixture. Such dependencies would make tests order-dependent,
significantly harder to debug, and less trustworthy. By providing a fresh
instance for each test (unless explicitly specified otherwise using `#[once]`),
`rstest` upholds this cornerstone of reliable testing, ensuring each test
operates on a known, independent baseline. The `#[once]` attribute, discussed
later, provides an explicit mechanism to opt into shared fixture state when
isolation is not a concern, or when the cost of fixture creation is prohibitive.

## IV. Parameterized tests with `rstest`

`rstest` excels at creating parameterized tests, allowing a single test logic
to be executed with multiple sets of input data. This is achieved primarily
through the `#[case]` and `#[values]` attributes.

### A. Table-driven tests with `#[case]`: defining specific scenarios

The `#[case(…)]` attribute enables table-driven testing, where each `#[case]`
defines a specific scenario with a distinct set of input arguments for the test
function. Arguments within the test function that are intended to receive these
values must also be annotated with `#[case]`.

A classic example is testing the Fibonacci sequence:

```rust,no_run
use rstest::rstest;

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

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

For each `#[case(input_val, expected_val)]` line, `rstest` generates a
separate, independent test. If one case fails, the others are still executed,
and reported individually by the test runner. These generated tests are often
named by appending `::case_N` to the original test function name (e.g.,
`test_fibonacci::case_1`, `test_fibonacci::case_2`, etc.), which aids in
identifying specific failing cases. This individual reporting mechanism
provides clearer feedback than a loop within a single test, where the first
failure might obscure subsequent ones.

### B. Combinatorial testing with `#[values]`: generating test matrices

The `#[values(…)]` attribute is used on test function arguments to generate
tests for every possible combination of the provided values (the Cartesian
product). This is particularly useful for testing interactions between
different parameters or ensuring comprehensive coverage across various input
states.

Consider testing a state machine's transition logic based on current state and
an incoming event:

```rust,no_run
use rstest::rstest;

#
enum State { Init, Start, Processing, Terminated }
#
enum Event { Process, Error, Fatal }

impl State {
    fn process(self, event: Event) -> Self {
        match (self, event) {
            (State::Init, Event::Process) => State::Start,
            (State::Start, Event::Process) => State::Processing,
            (State::Processing, Event::Process) => State::Processing,
            (_, Event::Error) => State::Start, // Example: error resets to Start
            (_, Event::Fatal) => State::Terminated,
            (s, _) => s, // No change for other combinations
        }
    }
}

#[rstest]
fn test_state_transitions(
    initial_state: State,
    #[values(Event::Process, Event::Error, Event::Fatal)] event: Event
) {
    // Real tests typically include more specific assertions based on expected_next_state
    let next_state = initial_state.process(event);
    println!("Testing: {:?} + {:?} -> {:?}", initial_state, event, next_state);
    // For demonstration, a generic assertion:
    assert!(true); // Replace with actual assertions
}
```

In this scenario, `rstest` will generate 3×3=9 individual test cases, covering
all combinations of `initial_state` and `event` specified in the `#[values]`
attributes.

It is important to be mindful that the number of generated tests can grow very
rapidly with `#[values]`. If a test function has three arguments, each with ten
values specified via `#[values]`, 10×10×10=1000 tests will be generated. This
combinatorial explosion can significantly impact test execution time and even
compile times. Developers must balance the desire for exhaustive combinatorial
coverage against these practical constraints, perhaps by selecting
representative values or using `#[case]` for more targeted scenarios.

### C. Using fixtures within parameterized tests

Fixtures can be seamlessly combined with parameterized arguments (`#[case]` or
`#[values]`) in the same test function. This powerful combination allows for
testing different aspects of a component (varied by parameters) within a
consistent environment or context (provided by fixtures). The "Complete
Example" in the `rstest` documentation hints at this synergy, stating that all
features can be used together, mixing fixture variables, fixed cases, and value
lists.

For example, a test might use a fixture to obtain a database connection and
then use `#[case]` arguments to test operations with different user IDs:

```rust,no_run
use rstest::*;

// Assume UserDb and User types are defined elsewhere
// #[fixture]
// fn db_connection() -> UserDb { UserDb::new() }

// #[rstest]
// fn test_user_retrieval(db_connection: UserDb, #[case] user_id: u32, #[case] expected_name: Option<&str>) {
//     let user = db_connection.fetch_user(user_id);
//     assert_eq!(user.map(|u| u.name), expected_name.map(String::from));
// }
```

In such a setup, the fixture provides the "stable" part of the test setup (the
`db_connection`), while `#[case]` provides the "variable" parts (the specific
`user_id` and `expected_name`). `rstest` resolves each argument independently:
if an argument name matches a fixture, it's injected; if it's marked with
`#[case]` or `#[values]`, it's populated from the parameterization attributes.
This enables rich and expressive test scenarios.

## V. Advanced fixture techniques

`rstest` offers several advanced features for defining and using fixtures,
providing greater control, reusability, and clarity.

### A. Composing fixtures: fixtures depending on other fixtures

Fixtures can depend on other fixtures. This is achieved by simply listing one
fixture as an argument to another fixture function. `rstest` will resolve this
dependency graph, ensuring that prerequisite fixtures are evaluated first. This
allows for the construction of complex setup logic from smaller, modular, and
reusable fixture components.

```rust,no_run
use rstest::*;

#[fixture]
fn base_value() -> i32 { 10 }

#[fixture]
fn derived_value(base_value: i32) -> i32 {
    base_value * 2
}

#[fixture]
fn configured_item(derived_value: i32, #[default("item_")] prefix: String) -> String {
    format!("{}{}", prefix, derived_value)
}

#[rstest]
fn test_composed_fixture(configured_item: String) {
    assert_eq!(configured_item, "item_20");
}

#[rstest]
fn test_composed_fixture_with_override(#[with("special_")] configured_item: String) {
    assert_eq!(configured_item, "special_20");
}
```

In this example, `derived_value` depends on `base_value`, and `configured_item`
depends on `derived_value`. When `test_composed_fixture` requests
`configured_item`, `rstest` first calls `base_value()`, then
`derived_value(10)`, and finally `configured_item(20, "item_".to_string())`.
This hierarchical dependency resolution mirrors good software design
principles, promoting modularity, maintainability, and clarity in test setups.

### B. Controlling fixture initialization: `#[once]` for shared state

For fixtures that are expensive to create or represent read-only shared data,
`rstest` provides the `#[once]` attribute. A fixture marked `#[once]` is
initialized only a single time, and all tests using it will receive a static
reference to this shared instance.

```rust,no_run
use rstest::*;
use std::sync::atomic::{AtomicUsize, Ordering};

#[fixture]
#[once]
fn expensive_setup() -> &'static AtomicUsize {
    // Simulate expensive setup
    println!("Performing expensive_setup once…");
    static COUNTER: AtomicUsize = AtomicUsize::new(0);
    COUNTER.fetch_add(1, Ordering::Relaxed); // To demonstrate it's called once
    &COUNTER
}

#[rstest]
fn test_once_1(expensive_setup: &'static AtomicUsize) {
    assert_eq!(expensive_setup.load(Ordering::Relaxed), 1);
}

#[rstest]
fn test_once_2(expensive_setup: &'static AtomicUsize) {
    assert_eq!(expensive_setup.load(Ordering::Relaxed), 1);
}
```

When using `#[once]`, there are critical warnings:

1. **Resource Lifetime:** The value returned by an `#[once]` fixture is
   effectively promoted to a `static` lifetime and is **never dropped**. This
   means any resources it holds (e.g., file handles, network connections) that
   require explicit cleanup via `Drop` will not be cleaned up automatically at
   the end of the test suite. This makes `#[once]` fixtures best suited for
   truly passive data or resources whose cleanup is managed by the operating
   system upon process exit.
2. **Functional Limitations:** `#[once]` fixtures cannot be `async` functions
   and cannot be generic functions (neither with generic type parameters nor
   using `impl Trait` in arguments or return types).
3. **Attribute Propagation:** `rstest` macros currently drop `#[expect]`
   attributes. If a test relies on lint expectations, use `#[allow]` instead to
   silence false positives.

The "never dropped" behaviour arises because `rstest` typically creates a
`static` variable to hold the result of the `#[once]` fixture. `static`
variables in Rust live for the entire duration of the program, and their `Drop`
implementations are not usually called at program exit. This is a crucial
consideration for resource management.

### C. Renaming fixtures for clarity: the `#[from]` attribute

Sometimes a fixture's function name might be long and descriptive, but a
shorter alternative name is preferred for the argument in a test or another
fixture. The `#[from(original_fixture_name)]` attribute on an argument allows
renaming. This is particularly useful when destructuring the result of a
fixture.

```rust,no_run
use rstest::*;

#[fixture]
fn complex_user_data_fixture() -> (String, u32, String) {
    ("Alice".to_string(), 30, "Engineer".to_string())
}

#[rstest]
fn test_with_renamed_fixture(#[from(complex_user_data_fixture)] user_info: (String, u32, String)) {
    assert_eq!(user_info.0, "Alice");
}

#[rstest]
fn test_with_destructured_fixture(#[from(complex_user_data_fixture)] (name, _, _): (String, u32, String)) {
    assert_eq!(name, "Alice");
}
```

The `#[from]` attribute decouples the fixture's actual function name from the
variable name used within the consuming function. As shown, if a fixture
returns composite data—for instance tuples or structs—and the test only cares
about a subset of the data or needs to use more idiomatic names for
destructured elements, `#[from]` is essential to link the argument pattern to
the correct source fixture.

### D. Partial fixture injection & default arguments

`rstest` provides mechanisms for creating highly configurable "template"
fixtures using `#[default(…)]` for fixture arguments and `#[with(…)]` to
override these defaults on a per-test basis.

- `#[default(…)]`: Used within a fixture function's signature to provide
  default values for its arguments.
- `#[with(…)]`: Applied to a fixture argument in a test (or in another
  fixture) to supply explicit values and override any defaults.

```rust,no_run
use rstest::*;

struct User { name: String, age: u8, role: String }

impl User {
    fn new(name: impl Into<String>, age: u8, role: impl Into<String>) -> Self {
        User { name: name.into(), age, role: role.into() }
    }
}

#[fixture]
fn user_fixture(
    #[default("DefaultUser")] name: &str,
    #[default(30)] age: u8,
    #[default("Viewer")] role: &str,
) -> User {
    User::new(name, age, role)
}

#[rstest]
fn test_default_user(user_fixture: User) {
    assert_eq!(user_fixture.name, "DefaultUser");
    assert_eq!(user_fixture.age, 30);
    assert_eq!(user_fixture.role, "Viewer");
}

#[rstest]
fn test_admin_user(#[with("AdminUser", 42, "Admin")] user_fixture: User) {
    assert_eq!(user_fixture.name, "AdminUser");
    assert_eq!(user_fixture.age, 42);
    assert_eq!(user_fixture.role, "Admin");
}

#[rstest]
fn test_custom_name_user(#[with("SpecificName")] user_fixture: User) {
    assert_eq!(user_fixture.name, "SpecificName");
    assert_eq!(user_fixture.age, 30); // Age uses default
    assert_eq!(user_fixture.role, "Viewer"); // Role uses default
}
```

This pattern of `#[default]` in fixtures and `#[with]` in tests allows a small
number of flexible fixtures to serve a large number of test variations. It
promotes a Don't Repeat Yourself (DRY) approach to test setup by centralizing
common configurations in the fixture's defaults and allowing targeted
customization where needed, thus reducing the proliferation of slightly
different fixtures.

### E. "Magic" argument conversions (e.g., `FromStr`)

For convenience, if a type implements the `std::str::FromStr` trait, `rstest`
can often automatically convert string literals provided in `#[case]` or
`#[values]` attributes directly into an instance of that type.

An example is converting string literals to `std::net::SocketAddr`:

```rust,no_run
use rstest::*;
use std::net::SocketAddr;

#[rstest]
#[case("127.0.0.1:8080", 8080)]
#[case("192.168.1.1:9000", 9000)]
fn check_socket_port(#[case] addr: SocketAddr, #[case] expected_port: u16) {
    assert_eq!(addr.port(), expected_port);
}
```

In this test, `rstest` sees the argument `addr: SocketAddr` and the string
literal `"127.0.0.1:8080"`. It implicitly calls
`SocketAddr::from_str("127.0.0.1:8080")` to create the `SocketAddr` instance.
This "magic" conversion makes test definitions more concise and readable by
allowing the direct use of string representations for types that support it.
However, if the `FromStr` conversion fails (e.g., because of a malformed
string), the error will typically occur at test runtime, potentially leading to
a panic. For types with complex parsing logic or many failure modes, it might
be clearer to perform the conversion explicitly within a fixture or at the
beginning of the test to handle errors more gracefully or provide more specific
diagnostic messages.

## VI. Asynchronous testing with `rstest`

`rstest` provides robust support for testing asynchronous Rust code,
integrating with common async runtimes and offering syntactic sugar for
managing futures.

### A. Defining asynchronous fixtures (`async fn`)

Creating an asynchronous fixture is straightforward: simply define the fixture
function as an `async fn`. `rstest` will recognize it as an async fixture and
handle its execution accordingly when used in an async test.

```rust,no_run
use rstest::*;
use std::time::Duration;

#[fixture]
async fn async_data_fetcher() -> String {
    // Simulate an async operation
    async_std::task::sleep(Duration::from_millis(10)).await;
    "Fetched async data".to_string()
}

// This fixture will be used in an async test later.
```

The example above uses `async_std::task::sleep` purely as a convenient
stand-in; the fixture may call into whichever runtime the project adopts because
`rstest` simply awaits the returned future.

### B. Writing asynchronous tests (`async fn` with `#[rstest]`)

Test functions themselves can also be `async fn`. `rstest` polls the future the
test returns but does not install or default to an async runtime. Annotate the
test with the runtime's attribute (for example, `#[tokio::test]`,
`#[async_std::test]`, or `#[actix_rt::test]`) alongside `#[rstest]` so the
runtime drives execution.

```rust,no_run
use rstest::*;
use std::time::Duration;

#[fixture]
async fn async_fixture_value() -> u32 {
    async_std::task::sleep(Duration::from_millis(5)).await;
    100
}

#[rstest]
#[async_std::test] // Or #[tokio::test], #[actix_rt::test]
async fn my_async_test(async_fixture_value: u32) {
    // Simulate further async work in the test
    async_std::task::sleep(Duration::from_millis(5)).await;
    assert_eq!(async_fixture_value, 100);
}
```

The order of procedural macro attributes can sometimes matter. While `rstest`
documentation and examples show flexibility (e.g., `#[rstest]` then
`#[tokio::test]`, or vice versa), users should ensure their chosen async
runtime's test macro is correctly placed to provide the necessary execution
context for the async test body and any async fixtures. `rstest` itself does
not bundle a runtime; it integrates with existing ones. The "Inject Test
Attribute" feature mentioned in `rstest` documentation may offer more explicit
control over which test runner attribute is applied.

### C. Managing futures: `#[future]` and `#[awt]` attributes

To improve the ergonomics of working with async fixtures and values in tests,
`rstest` provides the `#[future]` and `#[awt]` attributes.

- `#[future]`: When an async fixture, or an async block, is passed as an
  argument, its type is `impl Future<Output = T>`. The `#[future]` attribute on
  argument allows developers to refer to it with type `T` directly in the test
  signature, removing the `impl Future` boilerplate. However, the value still
  needs to be `.await`ed explicitly within the test body or by using `#[awt]`.
- `#[awt]` (or `#[future(awt)]`): This attribute, when applied to the entire
  test function (`#[awt]`) or a specific `#[future]` argument
  (`#[future(awt)]`), tells `rstest` to automatically insert `.await` calls for
  those futures.

```rust,no_run
use rstest::*;
use std::time::Duration;

#[fixture]
async fn base_value_async() -> u32 {
    async_std::task::sleep(Duration::from_millis(1)).await;
    42
}

#[rstest]
#[case(async { 7 })]
#[async_std::test]
async fn test_with_future_awt_arg(
    #[future] base_value_async: u32,
    #[future(awt)] #[case] divisor_async: u32, // Only divisor_async is auto-awaited
) {
    // Make awaiting explicit for clarity:
    let base = base_value_async.await;
    assert_eq!(base / divisor_async, 6);
}

#[rstest]
#[case(async { 7 })]
#[async_std::test]
#[awt] // Applies await to every #[future] argument automatically.
async fn test_with_future_awt_arg_awt(
    #[future] base_value_async: u32,
    #[future] #[case] divisor_async: u32,
) {
    // #[awt] removes the need for manual .await calls.
    assert_eq!(base_value_async / divisor_async, 6);
}
```

These attributes significantly reduce boilerplate associated with async code,
making the test logic appear more synchronous and easier to read by abstracting
away some of the explicit `async`/`.await` mechanics.

### D. Test timeouts for async tests (`#[timeout]`)

Long-running, or stalled, asynchronous operations can cause tests to hang
indefinitely. `rstest` provides a `#[timeout(…)]` attribute to set a maximum
execution time for async tests. This feature typically relies on the
`async-timeout` feature of `rstest`, which is enabled by default.

```rust,no_run
use rstest::*;
use std::time::Duration;

async fn potentially_long_operation(duration: Duration) -> u32 {
    async_std::task::sleep(duration).await;
    42
}

#[rstest]
#
#[async_std::test]
async fn test_operation_within_timeout() {
    assert_eq!(potentially_long_operation(Duration::from_millis(10)).await, 42);
}

#[rstest]
#
#[async_std::test]
#[should_panic] // Expect this test to panic due to timeout
async fn test_operation_exceeds_timeout() {
    assert_eq!(potentially_long_operation(Duration::from_millis(100)).await, 42);
}
```

A default timeout for all `rstest` async tests can also be set using the
`RSTEST_TIMEOUT` environment variable (value in seconds), evaluated at test
compile time. This built-in timeout support is a practical feature for ensuring
test suite stability.

## VII. Working with external resources and test data

Tests often need to interact with the filesystem, databases, or network
services. `rstest` fixtures provide an excellent way to manage these external
resources and test data.

### A. Fixtures for temporary files and directories

Managing temporary files, and directories, is a common requirement for tests
that involve file input/output. While `rstest` itself doesn't directly provide
temporary file utilities, its fixture system integrates seamlessly with crates
like `tempfile` or `test-temp-dir`. A fixture can create a temporary file,
directory, or similar resource, expose whichever locator the test requires, and
ensure cleanup (often via Resource Acquisition Is Initialization (RAII)). That
locator might be a filesystem path or an open handle depending on the scenario.

Here's an illustrative example using the `tempfile` crate:

```rust,no_run
use rstest::*;
use tempfile::{tempdir, TempDir}; // Add tempfile = "3" to [dev-dependencies]
use std::fs::File;
use std::io::{Write, Read};
use std::path::PathBuf;

// Fixture that provides a temporary directory.
// The TempDir object ensures cleanup when it's dropped.
#[fixture]
fn temp_directory() -> TempDir {
    tempdir().expect("Failed to create temporary directory")
}

// Fixture that creates a temporary file with specific content within a temp directory.
// It depends on the temp_directory fixture.
#[fixture]
fn temp_file_with_content(
    #[from(temp_directory)] // Use #[from] if name differs or for clarity
    temp_dir: &TempDir, // Take a reference to ensure TempDir outlives this fixture's direct use
    #[default("Hello, rstest from a temp file!")] content: &str
) -> PathBuf {
    let file_path = temp_dir.path().join("my_temp_file.txt");
    let mut file = File::create(&file_path).expect("Failed to create temporary file");
    file.write_all(content.as_bytes()).expect("Failed to write to temporary file");
    file_path
}

#[rstest]
fn test_read_from_temp_file(temp_file_with_content: PathBuf) {
    assert!(temp_file_with_content.exists());
    let mut file = File::open(temp_file_with_content).expect("Failed to open temp file");
    let mut read_content = String::new();
    file.read_to_string(&mut read_content).expect("Failed to read temp file");
    assert_eq!(read_content, "Hello, rstest from a temp file!");
}
```

By encapsulating temporary resource management within fixtures, tests become
cleaner, more predictable, and less prone to errors related to resource setup
or cleanup. The RAII (Resource Acquisition Is Initialization) pattern, common
in Rust and exemplified by `tempfile::TempDir` (which cleans up the directory
when dropped), works effectively with `rstest`'s fixture model. When a regular
(non-`#[once]`) fixture returns a `TempDir` object, or an object that owns it,
the resource is typically cleaned up after the test finishes, as the fixture's
return value goes out of scope. This localizes resource management logic to the
fixture, keeping the test focused on its assertions. For temporary resources,
regular (per-test) fixtures are generally preferred over `#[once]` fixtures to
ensure proper cleanup, as `#[once]` fixtures are never dropped.

### B. Mocking external services (e.g., database connections, HTTP APIs)

For unit and integration tests that depend on external services like databases
or HTTP APIs, mocking is a crucial technique. Mocks allow tests to run in
isolation, without relying on real external systems, making them faster, more
reliable, and easier to reason about. `rstest` fixtures are an ideal place to
encapsulate the setup and configuration of mock objects. Crates like `mockall`
can be used to create mocks, or they can be hand-rolled. The fixture would then
provide the configured mock instance to the test. General testing advice also
strongly recommends mocking external dependencies. The `rstest` documentation
itself shows examples with fakes or mocks like `empty_repository` and
`string_processor`.

A conceptual example using a hypothetical mocking library:

```rust,no_run
use rstest::*;
use std::sync::Arc;

// Assume mockall or a similar library is used to define MockMyDatabase
// #[mockall::automock]
// pub trait MyDatabase {
//     fn get_user_name(&self, id: u32) -> Option<String>;
// }

// For demonstration, a simple manual mock:
pub trait MyDatabase {
    fn get_user_name(&self, id: u32) -> Option<String>;
}

pub struct MockMyDatabase {
    pub expected_id: u32,
    pub user_to_return: Option<String>,
    pub called: std::cell::Cell<bool>,
}

impl MyDatabase for MockMyDatabase {
    fn get_user_name(&self, id: u32) -> Option<String> {
        self.called.set(true);
        if id == self.expected_id {
            self.user_to_return.clone()
        } else {
            None
        }
    }
}

// Fixture that provides a pre-configured mock database
#[fixture]
fn mock_db_returns_alice() -> MockMyDatabase {
    MockMyDatabase {
        expected_id: 1,
        user_to_return: Some("Alice".to_string()),
        called: std::cell::Cell::new(false),
    }
}

// A service that uses the database
struct UserService {
    db: Arc<dyn MyDatabase + Send + Sync>, // Use Arc<dyn Trait> for shared ownership
}

impl UserService {
    fn new(db: Arc<dyn MyDatabase + Send + Sync>) -> Self {
        UserService { db }
    }

    fn fetch_username(&self, id: u32) -> Option<String> {
        self.db.get_user_name(id)
    }
}

#[rstest]
fn test_user_service_with_mock_db(mock_db_returns_alice: MockMyDatabase) {
    let user_service = UserService::new(Arc::new(mock_db_returns_alice));
    assert_eq!(user_service.fetch_username(1), Some("Alice".to_string()));
    // Accessing mock_db_returns_alice.called directly here is problematic due to move.
    // In a real mockall scenario, expectations would be checked on the mock object.
}
```

Placing mock setup logic within fixtures hides its complexity (which can be
verbose, involving defining expectations, return values, and call counts) from
the actual test function. Tests then simply request the configured mock as an
argument. If different tests require the mock to behave differently, multiple
specialized mock fixtures can be created, or fixture arguments combined with
`#[with(…)]` can be used to dynamically configure the mock's behaviour within
the fixture itself. This makes tests that depend on external services more
readable and maintainable.

### C. Using `#[files(…)]` for test input from filesystem paths

For tests that need to process data from multiple input files, `rstest`
provides the `#[files("glob_pattern")]` attribute. This attribute can be used
on a test function argument to inject file paths that match a given glob
pattern. The argument type is typically `PathBuf`. It can also inject file
contents directly as `&str` or `&[u8]` by specifying a mode, e.g.,
`#[files("glob_pattern", mode = "str")]`, and additional attributes such as
`#[base_dir = "…"]` can specify a base directory for the glob, and
`#[exclude("regex")]` can filter out paths matching a regular expression.

```rust,no_run
use rstest::*;
use std::path::PathBuf;
use std::fs;

// Assume the fixture directory contains files like `file1.txt`, `file2.json`

#[rstest]
#[files("tests/test_data/*.txt")] // Injects PathBuf for each.txt file
fn process_text_file(#[files] path: PathBuf) {
    println!("Processing file: {:?}", path);
    let content = fs::read_to_string(path).expect("Could not read file");
    assert!(!content.is_empty());
}

#[rstest]
#[files("tests/test_data/*.json", mode = "str")] // Injects content of each.json file as &str
fn process_json_content(#[files] content: &str) {
    println!("Processing JSON content (first 50 chars): {:.50}", content);
    assert!(content.contains("{")); // Basic check for JSON-like content
}
```

The `#[files]` attribute effectively parameterizes a test over a set of files
discovered at compile time. For each file matching the glob, `rstest` generates
a separate test case, injecting the `PathBuf` or content into the designated
argument. This is powerful for data-driven testing where inputs reside in
separate files. When using `mode = "str"` or `mode = "bytes"`, `rstest` uses
`include_str!` or `include_bytes!` respectively. This embeds the file content
directly into the compiled binary, which is convenient for small files, but it
can significantly increase binary size if used with large data files.

## VIII. Reusability and organization

As test suites grow, maintaining reusability, clear organization, and
predictable execution becomes paramount. `rstest` and its ecosystem provide
tools and encourage practices that support these goals.

### A. Leveraging `rstest_reuse` for test templates

While `rstest`'s `#[case]` attribute is excellent for parameterization,
repeating the same set of `#[case]` attributes across multiple test functions
can lead to duplication. The `rstest_reuse` crate addresses this by allowing
the definition of reusable test templates.

`rstest_reuse` introduces two main attributes:

- `#[template]`: Used to define a named template that encapsulates a set of
  `#[rstest]` attributes, typically `#[case]` definitions.
- `#[apply(template_name)]`: Used on a test function to apply a previously
  defined template, effectively injecting its attributes.

```rust,no_run
// Add to Cargo.toml: rstest_reuse = "0.7" (or latest)
// In the test module or lib.rs/main.rs for crate-wide visibility if needed:
// #[cfg(test)]
// use rstest_reuse; // Important for template macro expansion

use rstest::rstest;
use rstest_reuse::{self, template, apply}; // Or use rstest_reuse::*;

// Define a template with common test cases
#[template]
#[rstest]
#[case(2, 2)]
#[case(4 / 2, 2)] // Cases can use expressions
#[case(6, 3 * 2)]
fn common_math_cases(#[case] a: i32, #[case] b: i32) {}

// Apply the template to a test function
#[apply(common_math_cases)]
fn test_addition_is_commutative(#[case] a: i32, #[case] b: i32) {
    assert_eq!(a + b, b + a);
}

// Apply the template to another test function, possibly with additional cases
#[apply(common_math_cases)]
#[case(10, 5 + 5)] // Composition: add more cases
fn test_multiplication_by_one(#[case] a: i32, #[case] b: i32) {
    // This test might not use 'b', but the template provides it.
    assert_eq!(a * 1, a);
    assert_eq!(b * 1, b); // Example of using b
}
```

`rstest_reuse` works by having `#[template]` define a macro. When
`#[apply(template_name)]` is used, this macro is called and expands to the set
of attributes (like `#[case]`) onto the target function. This meta-programming
technique effectively avoids direct code duplication of parameter sets,
promoting DRY principles in test case definitions. `rstest_reuse` also supports
composing templates with additional `#[case]` or `#[values]` attributes when
applying them.

### B. Best practices for organizing fixtures and tests

Good fixture and test organization mirrors good software design principles. As
the number of tests and fixtures grows, a well-structured approach is critical
for maintainability and scalability.

- **Placement:**
  - For fixtures used within a single module, they can be defined within that
    module's `tests` submodule (annotated with `#[cfg(test)]`).
  - For fixtures intended to be shared across multiple integration test files
    (in the `tests/` directory), consider creating a common module within the
    `tests/` directory (e.g., `tests/common/fixtures.rs`) and re-exporting
    public fixtures.
  - Alternatively, define shared fixtures in the library crate itself (e.g., in
    `src/lib.rs` or `src/fixtures.rs` under `#[cfg(test)]`) and `use` them in
    integration tests.
- **Naming Conventions:** Use clear, descriptive names for fixtures that
  indicate what they provide or set up. Test function names should clearly
  state what behaviour they are verifying.
- **Fixture Responsibility:** Aim for fixtures with a single, well-defined
  responsibility. Complex setups can be achieved by composing smaller, focused
  fixtures.
- **Scope Management (**`#[once]` **vs. Regular):** Make conscious decisions
  about fixture lifetimes. Use `#[once]` sparingly, only for genuinely
  expensive, read-only, and safely static resources, being mindful of its
  "never dropped" nature. Prefer regular (per-test) fixtures for test isolation
  and proper resource management.
- **Modularity:** Group related fixtures and tests into modules. This improves
  navigation and understanding of the test suite.
  - **Readability:** Utilize features like `#[from]` for renaming and
    `#[default]` / `#[with]` for configurable fixtures to enhance the clarity
    of both fixture definitions and their usage in tests.

General testing advice, such as keeping tests small, focused, and
deterministic, and mocking external dependencies, also applies and is
well-supported by `rstest`'s design.

## IX. `rstest` in context: comparison and considerations

Understanding how `rstest` compares to standard Rust testing approaches and its
potential trade-offs helps in deciding when and how to best utilize it.

### A. `rstest` vs. standard Rust `#[test]` and manual setup

Standard Rust testing using just the `#[test]` attribute is functional, but it
can become verbose for scenarios involving shared setup or parameterization.
`rstest` offers significant improvements in these areas:

- **Fixture Management:** With standard `#[test]`, shared setup typically
  involves calling helper functions manually at the beginning of each test.
  `rstest` automates this via declarative fixture injection.
- **Parameterization:** Achieving table-driven tests with standard `#[test]`
  often requires writing loops inside a single test function (which has poor
  failure reporting for individual cases) or creating multiple distinct
  `#[test]` functions with slight variations. `rstest`'s `#[case]` and
  `#[values]` attributes provide a much cleaner and more powerful solution.
- **Readability and Boilerplate:** `rstest` generally leads to less boilerplate
  code, and more readable tests, because dependencies are explicit in the
  function signature, and parameterization is handled declaratively.

The following table summarizes key differences:

**Table 1:** `rstest` vs standard Rust `#[test]` for fixture management and
parameterisation

| Feature                                  | Standard #[test] Approach                                     | rstest Approach                                                                  |
| ---------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Fixture Injection                        | Manual calls to setup functions within each test.             | Fixture name as argument in #[rstest] function; fixture defined with #[fixture]. |
| Parameterized Tests (Specific Cases)     | Loop inside one test, or multiple distinct #[test] functions. | #[case(…)] attributes on #[rstest] function.                                     |
| Parameterized Tests (Value Combinations) | Nested loops inside one test, or complex manual generation.   | #[values(…)] attributes on arguments of #[rstest] function.                      |
| Async Fixture Setup                      | Manual async block and .await calls inside test.              | async fn fixtures, with #[future] and #[awt] for ergonomic `.await`ing.          |
| Reusing Parameter Sets                   | Manual duplication of cases or custom helper macros.          | rstest_reuse crate with #[template] and #[apply] attributes.                     |

This comparison highlights how `rstest`'s attribute-based, declarative approach
streamlines common testing patterns, reduces manual effort, and improves the
clarity of test intentions.

### B. When to choose `rstest`

`rstest` is particularly beneficial in the following scenarios:

- **Complex Setups:** When tests require non-trivial setup or shared state
  (e.g., database connections, mock servers, complex data structures).
- **Parameterized Testing:** When a piece of logic needs to be tested against
  numerous input combinations or specific edge cases.
- **Improved Readability:** Use fixtures when the dependencies must be
  immediately obvious from the function signature.
- **DRY Principles:** When looking to reduce boilerplate and avoid duplication
  in test setup and parameter definitions.

For very simple unit tests that have no shared setup and require no
parameterization (e.g., testing a pure function with a single input), the
standard `#[test]` attribute might be sufficient. The overhead of learning and
integrating `rstest` (including its macro-driven nature) is most justified when
the complexity it helps manage is significant.

### C. Potential considerations

While `rstest` offers many advantages, some considerations should be kept in
mind:

- **Macro Expansion Impact:** Procedural macros, by their nature, involve code
  generation at compile time. This can sometimes lead to longer compilation
  times for test suites, especially large ones. Debugging macro-related issues
  can also be less straightforward if the developer is unfamiliar with how the
  macros expand.
- **Debugging Parameterized Tests:** `rstest` generates individual test
  functions for parameterized cases, often named like
  `test_function_name::case_N`. Understanding this naming convention is helpful
  for identifying and running specific failing cases with
  `cargo test test_function_name::case_N`. Some IDEs, or debuggers, might
  require specific configurations or might not fully support stepping through
  macro-generated code as seamlessly as handwritten code, though support is
  improving.
- **Static Nature of Test Cases:** Test cases (e.g., from `#[case]` or
  `#[files]`) are defined and discovered at compile time. This means the
  structure of the tests is validated by the Rust compiler, which can catch
  structural errors (like type mismatches in `#[case]` arguments or references
  to non-existent fixtures) earlier than runtime test discovery mechanisms.
  This compile-time validation is a strength, offering a degree of static
  verification for the test suite itself. However, it also means that
  dynamically generating test cases at runtime based on external factors (not
  known at compile time) is not directly supported by `rstest`'s core model.
- `no_std` **Support:** `rstest` generally relies on the standard library
  (`std`) being available, as test runners and many common testing utilities
  depend on `std`. Therefore, it is typically not suitable for testing
  `#! [no_std]` libraries in a truly `no_std` test environment where the test
  harness itself cannot link `std`.
- **Learning Curve:** While designed for simplicity in basic use cases, the full
  range of attributes and advanced features (e.g., fixture composition, partial
  injection, async management attributes) has a learning curve.

## X. Ecosystem and advanced integrations

The `rstest` ecosystem includes helper crates that extend its functionality for
specific needs like logging and conditional test execution.

### A. `rstest-log`: logging in `rstest` tests

For developers who rely on logging frameworks like `log` or `tracing` for
debugging tests, the `rstest-log` crate can simplify integration. Test runners
often capture standard output and error streams, and logging frameworks require
proper initialization. `rstest-log` likely provides attributes, or wrappers, to
ensure that logging is correctly set up before each `rstest`-generated test
case runs, making it easier to get consistent log output from tests.

### B. `logtest`: verifying log output

`logtest` provides a lightweight logger that records emitted log records during
tests. This makes it trivial to assert on log messages without interfering with
other tests. Add it under `[dev-dependencies]` using an explicit version range:

```toml
[dev-dependencies]
logtest = "^2.0"
```

Start a `Logger` before running the code under test:

```rust,no_run
use logtest::Logger;

let mut logger = Logger::start();
my_async_fn().await;
assert!(logger.pop().is_some());
```

This crate complements `rstest` nicely when verifying that warnings, or errors,
are logged under specific conditions.

### C. `test-with`: conditional testing with `rstest`

The `test-with` crate allows for conditional execution of tests based on
various runtime conditions, including environment variables; specific files or
folders; and the availability of network services. It can be used with
`rstest`. For example, an `rstest` test could be further annotated with
`test-with` attributes to ensure it only runs if a particular database
configuration file exists, or if a dependent web service is reachable. The
order of macros is important: `rstest` should typically generate the test cases
first, and then `test-with` can apply its conditional execution logic to these
generated tests. This allows `rstest` to focus on test structure and data
provision, while `test-with` provides an orthogonal layer of control over test
execution conditions.

## XI. Conclusion and further resources

`rstest` significantly enhances the testing experience in Rust by providing a
powerful and expressive framework for fixture-based and parameterized testing.
Its declarative syntax, enabled by procedural macros, reduces boilerplate,
improves test readability, and promotes reusability of test setup logic. From
simple value injection and table-driven tests to complex fixture compositions,
asynchronous testing, and integration with the broader ecosystem, `rstest`
equips developers with the tools to build comprehensive and maintainable test
suites.

While considerations such as compile-time impact and the learning curve for
advanced features exist, the benefits in terms of cleaner, more robust, and
more expressive tests often outweigh these for projects with non-trivial
testing requirements.

### A. Recap of `rstest`'s power for fixture-based testing

`rstest` empowers Rust developers by:

- Simplifying dependency management in tests through fixture injection.
- Enabling concise and readable parameterized tests with `#[case]` and
  `#[values]`.
- Supporting advanced fixture patterns like composition, `#[once]` for shared
  state, renaming, and partial injection.
- Providing seamless support for asynchronous tests and fixtures, including
  ergonomic future management.
- Facilitating interaction with external resources through fixtures that can
  manage temporary files or mock objects.
- Allowing test case reuse via the `rstest_reuse` crate.

### B. Pointers to official documentation and community resources

For further exploration and the most up-to-date information, the following
resources are recommended:

- **Official** `rstest` **Documentation:** <https://docs.rs/rstest/>
- `rstest` **GitHub Repository:** <https://github.com/la10736/rstest>
- `rstest_reuse` **Crate:** <https://crates.io/crates/rstest_reuse>
- **Rust Community Forums:** Platforms like the Rust Users Forum
  (users.rust-lang.org) and Reddit (e.g., r/rust) may contain discussions and
  community experiences with `rstest`.

The following table provides a quick reference to some of the key attributes
provided by `rstest`:

**Table 2:** Key `rstest` attributes quick reference

| Attribute                  | Core Purpose                                                                                 |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| #[rstest]                  | Marks a function as a `rstest` test; enables fixture injection and parameterization.         |
| #[fixture]                 | Defines a function that provides a test fixture (setup data or services).                    |
| #[case(…)]                 | Defines a single parameterized test case with specific input values.                         |
| #[values(…)]               | Defines a list of values for an argument, generating tests for each value or combination.    |
| #[once]                    | Marks a fixture to be initialized only once and shared (as a static reference) across tests. |
| #[future]                  | Simplifies async argument types by removing impl Future boilerplate.                         |
| #[awt]                     | (Function or argument level) Automatically .awaits future arguments in async tests.          |
| #[from(original_name)]     | Allows renaming an injected fixture argument in the test function.                           |
| #[with(…)]                 | Overrides default arguments of a fixture for a specific test.                                |
| #[default(…)]              | Provides default values for arguments within a fixture function.                             |
| #[timeout(…)]              | Sets a timeout for an asynchronous test.                                                     |
| #[files("glob_pattern",…)] | Injects file paths (or contents, with mode=) matching a glob pattern as test arguments.      |

By mastering `rstest`, Rust developers can significantly elevate the quality
and efficiency of their testing practices, leading to more reliable,
maintainable, and predictable software.
