Guides · 5 of 7
Async scenarios
Steps can be async fn. Where they run, and whether they may wait on anything, depends on which of two forms binds the scenario.
An async scenario
Declare the scenario function async fn and put
#[tokio::test(flavor = "current_thread")] under
#[scenario]. Its steps may then be
async fn and await real work: timers, channels,
spawned tasks. They run in order on one thread, so a borrowed
fixture stays valid across an .await.
Feature: Tea at the picnic
Scenario: The kettle boils before the tea is poured
Given a kettle on the camp stove
When the kettle comes to the boil
Then there is hot water for 4 cups
Compiled and run against rstest-bdd 0.6.0.
//! An async `#[when]` step under `#[tokio::test]`, awaiting a channel until
//! the kettle boils.
use std::time::Duration;
use rstest::fixture;
use rstest_bdd_macros::{given, scenario, then, when};
use tokio::sync::watch;
struct Kettle {
boiled: watch::Sender<bool>,
cups: u32,
}
#[fixture]
fn kettle() -> Kettle {
let (boiled, _) = watch::channel(false);
Kettle { boiled, cups: 0 }
}
#[given("a kettle on the camp stove")]
fn on_the_stove(kettle: &mut Kettle) {
kettle.cups = 4;
}
#[when("the kettle comes to the boil")]
async fn boil(kettle: &Kettle) {
let mut whistle = kettle.boiled.subscribe();
let stove = kettle.boiled.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
stove.send_replace(true);
});
whistle.wait_for(|boiled| *boiled).await.expect("the stove went out");
}
#[then("there is hot water for {cups:u32} cups")]
fn pour(kettle: &Kettle, cups: u32) {
assert!(*kettle.boiled.borrow());
assert_eq!(kettle.cups, cups);
}
#[scenario(path = "tests/features/kettle.feature")]
#[tokio::test(flavor = "current_thread")]
async fn kettle_boils(kettle: Kettle) {
let _ = kettle;
}
Compiled and run against rstest-bdd 0.6.0.
The When step spawns the stove, waits on a channel
until it reports the boil, and only then lets the
Then step pour. The crate needs its own
tokio dev-dependency for the test attribute.
tokio = { version = "1", features = ["macros", "rt", "sync", "time"] }
Compiled and run against rstest-bdd 0.6.0.
The Tokio harness
harness = rstest_bdd_harness_tokio::TokioHarness
keeps the scenario function synchronous and runs the steps inside
a Tokio runtime the harness owns. A step can reach the runtime
through #[harness_context], and an
async fn step that finishes without waiting runs as
written.
#[scenario(
path = "tests/features/stage.feature",
harness = rstest_bdd_harness_tokio::TokioHarness,
)]
fn tokio_stage(trolley: Trolley) {
let _ = trolley;
}
Compiled and run against rstest-bdd 0.6.0.
Choosing a form
| Need | Async scenario | Tokio harness |
|---|---|---|
| Steps await timers, channels, or I/O | Yes | No: a waiting step fails |
| Steps reach the runtime handle | Through Tokio itself | Through #[harness_context] |
| Scenario function | async fn |
fn |
Works with scenarios! |
No | Yes: harness = … |