Skip to content

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.

tests/features/kettle.feature
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.

tests/kettle.rs
//! 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.

Cargo.toml
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.

tests/stage.rs
#[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

Choosing between the two async forms
NeedAsync scenarioTokio 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 = …

Fine print