Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions docs/rust-testing-with-rstest-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,9 +540,12 @@ When using `#[once]`, there are critical warnings:
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
1. **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).
1. **Attribute Propagation:** `rstest` macros currently drop `#[expect]`
attributes. If you rely 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`
Expand Down Expand Up @@ -1165,13 +1168,13 @@ The following table summarizes key differences:
**Table 1:** `rstest` **vs. Standard Rust** `#[test]` **for Fixture Management
and Parameterization**

| Feature | Standard #[test] Approach | rstest Approach |
| 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. |
| 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, reducing manual effort and improving the
Expand Down Expand Up @@ -1332,20 +1335,20 @@ provided by `rstest`:

**Table 2: Key** `rstest` **Attributes Quick Reference**

| Attribute | Core Purpose |
| Attribute | Core Purpose |
| ---------------------------- | -------------------------------------------------------------------------------------------- |
| #[rstest] | Marks a function as an 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. |
| #[rstest] | Marks a function as an 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 and maintainable
Expand Down
39 changes: 28 additions & 11 deletions tests/app_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! They verify successful extraction and error handling when state is missing.

use rstest::{fixture, rstest};
use wireframe::extractor::{
ExtractError,
FromMessageRequest,
Expand All @@ -10,20 +11,36 @@ use wireframe::extractor::{
SharedState,
};

#[test]
fn shared_state_extractor_returns_data() {
let mut req = MessageRequest::default();
req.insert_state(5u32);
let mut payload = Payload::default();
let extracted = SharedState::<u32>::from_message_request(&req, &mut payload).unwrap();
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
#[fixture]
fn request() -> MessageRequest { MessageRequest::default() }

#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
#[fixture]
fn empty_payload() -> Payload<'static> { Payload::default() }

#[rstest]
fn shared_state_extractor_returns_data(
mut request: MessageRequest,
mut empty_payload: Payload<'static>,
) {
request.insert_state(5u32);
let extracted = SharedState::<u32>::from_message_request(&request, &mut empty_payload).unwrap();
assert_eq!(*extracted, 5);
}

#[test]
fn missing_shared_state_returns_error() {
let req = MessageRequest::default();
let mut payload = Payload::default();
let err = SharedState::<u32>::from_message_request(&req, &mut payload)
#[rstest]
fn missing_shared_state_returns_error(
request: MessageRequest,
mut empty_payload: Payload<'static>,
) {
let err = SharedState::<u32>::from_message_request(&request, &mut empty_payload)
.err()
.unwrap();
assert!(matches!(err, ExtractError::MissingState(_)));
Expand Down
20 changes: 16 additions & 4 deletions tests/connection_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,24 @@ use wireframe::{
};

#[fixture]
#[allow(unused_braces)]
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
fn queues() -> (PushQueues<u8>, wireframe::push::PushHandle<u8>) { PushQueues::bounded(8, 8) }

#[fixture]
#[allow(unused_braces)]
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
fn shutdown_token() -> CancellationToken { CancellationToken::new() }

#[fixture]
#[allow(unused_braces)]
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
fn empty_stream() -> Option<FrameStream<u8, ()>> { None }

#[rstest]
Expand Down Expand Up @@ -341,7 +350,10 @@ impl std::ops::DerefMut for LoggerHandle {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.guard }
}

#[allow(unused_braces)]
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
#[fixture]
fn logger() -> LoggerHandle { LoggerHandle::new() }

Expand Down
53 changes: 30 additions & 23 deletions tests/extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,81 @@

use std::net::SocketAddr;

use rstest::{fixture, rstest};
use wireframe::{
extractor::{ConnectionInfo, FromMessageRequest, Message, MessageRequest, Payload},
message::Message as MessageTrait,
};

#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
#[fixture]
fn request() -> MessageRequest { MessageRequest::default() }

#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
#[fixture]
fn empty_payload() -> Payload<'static> { Payload::default() }

#[derive(bincode::Encode, bincode::BorrowDecode, PartialEq, Debug)]
struct TestMsg(u8);

#[test]
/// Tests that a message can be extracted from a payload and that the payload cursor advances fully.
///
/// Verifies that a `TestMsg` instance serialised into bytes can be correctly extracted from a
/// `Payload` using `Message::<TestMsg>::from_message_request`, and asserts that the payload has no
/// remaining unread data after extraction.
fn message_extractor_parses_and_advances() {
#[rstest]
fn message_extractor_parses_and_advances(request: MessageRequest) {
let msg = TestMsg(42);
let bytes = msg.to_bytes().unwrap();
let mut payload = Payload::new(bytes.as_slice());
let req = MessageRequest::default();

let extracted = Message::<TestMsg>::from_message_request(&req, &mut payload).unwrap();
let extracted = Message::<TestMsg>::from_message_request(&request, &mut payload).unwrap();
assert_eq!(*extracted, msg);
assert_eq!(payload.remaining(), 0);
}

#[test]
#[rstest]
/// Tests that `ConnectionInfo` correctly reports the peer socket address extracted from a
/// `MessageRequest`.
fn connection_info_reports_peer() {
fn connection_info_reports_peer(mut request: MessageRequest, mut empty_payload: Payload<'static>) {
let addr: SocketAddr = "127.0.0.1:12345"
.parse()
.expect("hard-coded socket address must be valid");
let req = MessageRequest {
peer_addr: Some(addr),
..Default::default()
};
let mut payload = Payload::default();
let info = ConnectionInfo::from_message_request(&req, &mut payload).unwrap();
request.peer_addr = Some(addr);
let info = ConnectionInfo::from_message_request(&request, &mut empty_payload).unwrap();
assert_eq!(info.peer_addr(), Some(addr));
}

#[test]
/// Tests that shared state of type `u8` can be successfully extracted from a `MessageRequest`'s
/// `app_data`.
///
/// Inserts an `Arc<u8>` into the request's shared state, extracts it using the `SharedState`
/// extractor, and asserts that the extracted value matches the original.
fn shared_state_extractor() {
let mut req = MessageRequest::default();
req.insert_state(42u8);
let mut payload = Payload::default();
#[rstest]
fn shared_state_extractor(mut request: MessageRequest, mut empty_payload: Payload<'static>) {
request.insert_state(42u8);

let state =
wireframe::extractor::SharedState::<u8>::from_message_request(&req, &mut payload).unwrap();
wireframe::extractor::SharedState::<u8>::from_message_request(&request, &mut empty_payload)
.unwrap();
assert_eq!(*state, 42);
}

#[test]
/// Tests that extracting a missing shared state from a `MessageRequest`
/// returns an `ExtractError::MissingState` containing the type name.
///
/// Ensures that when no shared state of the requested type is present,
/// the correct error is produced and includes the expected type information.
fn shared_state_missing_error() {
let req = MessageRequest::default();
let mut payload = Payload::default();
#[rstest]
fn shared_state_missing_error(request: MessageRequest, mut empty_payload: Payload<'static>) {
let Err(err) =
wireframe::extractor::SharedState::<u8>::from_message_request(&req, &mut payload)
wireframe::extractor::SharedState::<u8>::from_message_request(&request, &mut empty_payload)
else {
panic!("expected error");
};
Expand Down
10 changes: 8 additions & 2 deletions tests/push_policies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ impl std::ops::DerefMut for LoggerHandle {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.guard }
}

#[allow(unused_braces)]
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
#[fixture]
fn logger() -> LoggerHandle { LoggerHandle::new() }

#[allow(unused_braces)]
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
#[fixture]
fn rt() -> Runtime {
tokio::runtime::Builder::new_current_thread()
Expand Down
10 changes: 8 additions & 2 deletions tests/session_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ use wireframe::{
};

#[fixture]
#[allow(unused_braces)]
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
fn registry() -> SessionRegistry<u8> { SessionRegistry::default() }

#[fixture]
#[allow(unused_braces)]
#[allow(
unused_braces,
reason = "rustc false positive for single line rstest fixtures"
)]
fn push_setup() -> (PushQueues<u8>, PushHandle<u8>) { PushQueues::bounded(1, 1) }

/// Test that handles can be retrieved whilst the connection remains alive.
Expand Down