`, `| ` and ` | ` tags are detected.
Attributes and tag casing are ignored, and complex nested or styled tables are
@@ -10,3 +10,9 @@ not supported. After conversion each HTML table is represented as a Markdown
table so the usual reflow algorithm can align its columns consistently with the
rest of the document.
+```html
+
+```
diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md
index ea95b799..b35ef015 100644
--- a/docs/rust-testing-with-rstest-fixtures.md
+++ b/docs/rust-testing-with-rstest-fixtures.md
@@ -1,65 +1,135 @@
# 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 (`github.com/la10736/rstest`) emerges as a powerful solution, offering a sophisticated fixture-based and parameterized testing framework that significantly simplifies these tasks through the use of procedural macros.1 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.
+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 (`github.com/la10736/rstest`) emerges as a powerful solution, offering a
+sophisticated fixture-based and parameterized testing framework that
+significantly simplifies these tasks through the use of procedural macros.1 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.
## I. 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 there is a well-known and controlled environment in which tests are run so that results are repeatable. Test dependencies, such as database connections, user objects, or specific configurations, often require careful setup before a test can execute and, sometimes, teardown afterward. Managing this setup and teardown logic within each test function can lead to considerable boilerplate code and repetition, making tests harder to read and maintain.
-
-Fixtures address this by encapsulating these dependencies and their setup logic.1 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 and maintainable.
+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 there is a well-known and controlled environment in which
+tests are run so that results are repeatable. Test dependencies, such as
+database connections, user objects, or specific configurations, often require
+careful setup before a test can execute and, sometimes, teardown afterward.
+Managing this setup and teardown logic within each test function can lead to
+considerable boilerplate code and repetition, making tests harder to read and
+maintain.
+
+Fixtures address this by encapsulating these dependencies and their setup
+logic.1 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 and maintainable.
### 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.1 It is available on `crates.io` and its source code is hosted at `github.com/la10736/rstest` 3, distinguishing it from other software projects that may share the same name but operate in different ecosystems (e.g., a JavaScript/TypeScript framework mentioned in 5).
-
-The `rstest` crate utilizes Rust's procedural macros, such as `#[rstest]` and `#[fixture]`, to achieve its declarative and expressive syntax.2 These macros allow developers to define fixtures and inject them into test functions simply by listing them as arguments. This compile-time mechanism analyzes 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 the resolution and 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.7 Furthermore, understanding the macro expansion can sometimes be necessary for debugging complex test scenarios or unexpected behavior.8
+`rstest` is a Rust crate specifically designed to simplify and enhance testing
+by leveraging the concept of fixtures and providing powerful parameterization
+capabilities.1 It is available on `crates.io` and its source code is hosted at
+`github.com/la10736/rstest` 3, distinguishing it from other software projects
+that may share the same name but operate in different ecosystems (e.g., a
+JavaScript/TypeScript framework mentioned in 5).
+
+The `rstest` crate utilizes Rust's procedural macros, such as `#[rstest]` and
+`#[fixture]`, to achieve its declarative and expressive syntax.2 These macros
+allow developers to define fixtures and inject them into test functions simply
+by listing them as arguments. This compile-time mechanism analyzes 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 the resolution and 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.7 Furthermore,
+understanding the macro expansion can sometimes be necessary for debugging
+complex test scenarios or unexpected behavior.8
### 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.9 The test function's signature clearly documents what it needs to run. This allows developers to "focus on the important stuff in your tests" by abstracting away the setup details.1
-- **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.
+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.9 The test
+ function's signature clearly documents what it needs to run. This allows
+ developers to "focus on the important stuff in your tests" by abstracting away
+ the setup details.1
+- **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.
+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, not when building the main application or library.
-
-Add the following lines to your `Cargo.toml` under the `[dev-dependencies]` section:
+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, not when building the main application or library.
-Ini, TOML
+Add the following lines to your `Cargo.toml` under the `[dev-dependencies]`
+section:
-```
+```toml
[dev-dependencies]
rstest = "0.18" # Or the latest version available on crates.io
# rstest_macros may also be needed explicitly depending on usage or version
# rstest_macros = "0.18" # 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).1 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 and reduces compile times for non-test builds.11
+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).1 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 and reduces compile times for
+non-test builds.11
### B. Your 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.
+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
+```rust
-```
use rstest::fixture; // Or use rstest::*;
#[fixture]
@@ -68,17 +138,26 @@ pub fn answer_to_life() -> u32 {
}
```
-In this example, `answer_to_life` is a public function marked with `#[fixture]`. It takes no arguments and returns a `u32` value of 42.9 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.1 Fixtures can also depend on other fixtures, allowing for compositional setup.12
+In this example, `answer_to_life` is a public function marked with `#[fixture]`.
+It takes no arguments and returns a `u32` value of 42.9 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.1 Fixtures can
+also depend on other fixtures, allowing for compositional setup.12
### 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.
+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
+```rust
-```
use rstest::{fixture, rstest}; // Or use rstest::*;
#[fixture]
@@ -92,51 +171,63 @@ fn test_with_fixture(answer_to_life: u32) {
}
```
-In `test_with_fixture`, the argument `answer_to_life: u32` signals to `rstest` that the `answer_to_life` fixture should be injected.1 `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.13
+In `test_with_fixture`, the argument `answer_to_life: u32` signals to `rstest`
+that the `answer_to_life` fixture should be injected.1 `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.13
-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.1
+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.1
## III. Mastering Fixture Injection and Basic Usage
-Understanding how fixtures behave and how they can be structured is key to leveraging `rstest` effectively.
+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.1 This enables fixtures to encapsulate diverse setup logic, providing ready-to-use dependencies for tests.
+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.1 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
+```rust
- ```
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
+```rust
- ```
use rstest::*;
-
+
struct User {
id: u32,
name: String,
}
-
+
#[fixture]
fn sample_user() -> User {
User {
@@ -144,78 +235,99 @@ Here are a few examples illustrating different kinds of fixtures:
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
+- **Fixture performing setup and returning a resource (e.g., a mock
+ repository):**
+
+```rust
- ```
use rstest::*;
use std::collections::HashMap;
-
+
// A simple trait for a repository
trait Repository {
fn add_item(&mut self, id: &str, name: &str);
fn get_item_name(&self, id: &str) -> Option;
}
-
+
// A mock implementation
#
struct MockRepository {
data: HashMap,
}
-
+
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 {
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, adapted from concepts in 1 and 1, demonstrates a fixture providing a mutable `Repository` implementation.
+```
+
+This example, adapted from concepts in 1 and 1, demonstrates a fixture providing
+a mutable `Repository` implementation.
### B. Understanding Fixture Scope and Lifetime (Default Behavior)
-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 behavior 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.13
-
-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 behavior or failures in subsequent tests that use the same fixture. Such dependencies would make tests order-dependent and significantly harder to debug. 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 and 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.
+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 behavior 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.13
+
+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 behavior or failures in subsequent tests
+that use the same fixture. Such dependencies would make tests order-dependent
+and significantly harder to debug. 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 and
+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.
+`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]`.
+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 1:
-Rust
+```rust
-```
use rstest::rstest;
fn fibonacci(n: u32) -> u32 {
@@ -238,17 +350,27 @@ fn test_fibonacci(#[case] input: u32, #[case] expected: u32) {
}
```
-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.8 This individual reporting mechanism provides clearer feedback than a loop within a single test, where the first failure might obscure subsequent ones.
+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.8 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.1
+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.1
-Consider testing a state machine's transition logic based on current state and an incoming event:
+Consider testing a state machine's transition logic based on current state and
+an incoming event:
-Rust
+```rust
-```
use rstest::rstest;
#
@@ -282,19 +404,32 @@ fn test_state_transitions(
}
```
-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.1
+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.1
-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.
+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.9
+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.9
-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:
+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
+```rust
-```
use rstest::*;
// Assume UserDb and User types are defined elsewhere
@@ -308,19 +443,28 @@ use rstest::*;
// }
```
-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.
+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.
+`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.4
+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.4
-Rust
+```rust
-```
use rstest::*;
#[fixture]
@@ -347,15 +491,22 @@ fn test_composed_fixture_with_override(#[with("special_")] configured_item: Stri
}
```
-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 and maintainability in test setups.
+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 and maintainability 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.9
+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.9
-Rust
+```rust
-```
use rstest::*;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -382,18 +533,32 @@ fn test_once_2(expensive_setup: &'static AtomicUsize) {
When using `#[once]`, there are critical caveats 12:
-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).
-
-The "never dropped" behavior 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.
+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).
+
+The "never dropped" behavior 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 or different name is preferred for the argument in a test or another fixture. The `#[from(original_fixture_name)]` attribute on an argument allows renaming.12 This is particularly useful when destructuring the result of a fixture.
+Sometimes a fixture's function name might be long and descriptive, but a shorter
+or different name is preferred for the argument in a test or another fixture.
+The `#[from(original_fixture_name)]` attribute on an argument allows renaming.12
+This is particularly useful when destructuring the result of a fixture.
-Rust
+```rust
-```
use rstest::*;
#[fixture]
@@ -412,18 +577,26 @@ fn test_with_destructured_fixture(#[from(complex_user_data_fixture)] (name, _, _
}
```
-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 a tuple or struct and the test only cares about some parts or wants to use more idiomatic names for destructured elements, `#[from]` is essential to link the argument pattern to the correct source fixture.12
+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
+a tuple or struct and the test only cares about some parts or wants to use more
+idiomatic names for destructured elements, `#[from]` is essential to link the
+argument pattern to the correct source fixture.12
### D. Partial Fixture Injection and Default Arguments: `#[with]` and `#[default]`
-`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.
+`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 own arguments.4
-- `#[with(...)]`: Used on a test function's fixture argument (or a fixture argument within another fixture) to supply specific values to the parameters of the invoked fixture, overriding any defaults.4
+- `#[default(...)]`: Used within a fixture function's signature to provide
+ default values for its own arguments.4
+- `#[with(...)]`: Used on a test function's fixture argument (or a fixture
+ argument within another fixture) to supply specific values to the parameters
+ of the invoked fixture, overriding any defaults.4
-Rust
+```rust
-```
use rstest::*;
struct User { name: String, age: u8, role: String }
@@ -469,17 +642,23 @@ fn test_custom_name_user(# user_fixture: User) {
}
```
-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 DRY (Don't Repeat Yourself) 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.
+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 DRY (Don't Repeat Yourself) 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.1
+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.1
An example is converting string literals to `std::net::SocketAddr`:
-Rust
+```rust
-```
use rstest::*;
use std::net::SocketAddr;
@@ -491,19 +670,31 @@ fn check_socket_port(#[case] addr: SocketAddr, #[case] expected_port: u16) {
}
```
-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., due to 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.
+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., due to 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.
+`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`.12 `rstest` will recognize it as an async fixture and handle its execution accordingly when used in an async test.
+Creating an asynchronous fixture is straightforward: simply define the fixture
+function as an `async fn`.12 `rstest` will recognize it as an async fixture and
+handle its execution accordingly when used in an async test.
-Rust
+```rust
-```
use rstest::*;
use std::time::Duration;
@@ -517,15 +708,21 @@ async fn async_data_fetcher() -> String {
// This fixture will be used in an async test later.
```
-The example above uses `async_std::task::sleep`, aligning with `rstest`'s default async runtime support, but the fixture logic can be any async code.4
+The example above uses `async_std::task::sleep`, aligning with `rstest`'s
+default async runtime support, but the fixture logic can be any async code.4
### B. Writing Asynchronous Tests (`async fn` with `#[rstest]`)
-Test functions themselves can also be `async fn`. `rstest` will manage the execution of these async tests. By default, `rstest` often uses `#[async_std::test]` to annotate the generated async test functions.9 However, it is designed to be largely runtime-agnostic and can be integrated with other popular async runtimes like Tokio or Actix. This is typically done by adding the runtime's specific test attribute (e.g., `#[tokio::test]` or `#[actix_rt::test]`) alongside `#[rstest]`.4
+Test functions themselves can also be `async fn`. `rstest` will manage the
+execution of these async tests. By default, `rstest` often uses
+`#[async_std::test]` to annotate the generated async test functions.9 However,
+it is designed to be largely runtime-agnostic and can be integrated with other
+popular async runtimes like Tokio or Actix. This is typically done by adding the
+runtime's specific test attribute (e.g., `#[tokio::test]` or
+`#[actix_rt::test]`) alongside `#[rstest]`.4
-Rust
+```rust
-```
use rstest::*;
use std::time::Duration;
@@ -544,18 +741,32 @@ async fn my_async_test(async_fixture_value: u32) {
}
```
-The order of procedural macro attributes can sometimes matter.15 While `rstest` documentation and examples show flexibility (e.g., `#[rstest]` then `#[tokio::test]` 4, 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 10 may offer more explicit control over which test runner attribute is applied.
+The order of procedural macro attributes can sometimes matter.15 While `rstest`
+documentation and examples show flexibility (e.g., `#[rstest]` then
+`#[tokio::test]` 4, 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 10 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.
+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 |