Skip to content

Guides · 3 of 7

Fixtures and state

There is no World struct to define. Fixtures are the world: each scenario gets fresh ones, steps borrow them, and anything shared across scenarios is shared on purpose.

Fixtures are the world

A step parameter that is not a placeholder is a fixture, found by its name. The fixtures are ordinary rstest fixtures, so a unit test and a scenario can share one definition.

tests/lantern.rs
#[fixture]
fn trolley() -> Trolley {
    Trolley::default()
}

Compiled and run against rstest-bdd 0.6.0.

The scenario function names the fixtures the scenario owns, and every scenario builds its own. Two scenarios never see each other’s trolley, so they can run in parallel and in any order.

Changing a fixture

A step that takes &mut Trolley gets exclusive access to the scenario’s trolley for the length of the step; one that takes &Trolley reads it. No RefCell is needed for either.

tests/lantern.rs
#[when("the departure bell rings")]
fn depart(trolley: &mut Trolley) {
    trolley.arrived = true;
}

#[then("the lantern arrives upright")]
fn check(trolley: &Trolley) {
    assert!(trolley.loaded);
    assert!(trolley.arrived);
    assert!(trolley.upright);
}

Compiled and run against rstest-bdd 0.6.0.

A step can also return a new value instead, which replaces the fixture of that type; steps that return shows how.

Optional state in slots

Some state only exists once a step has run: what was borrowed, what a command printed. Slot<T> starts empty and is filled by set, read by get, and checked by is_empty, all through a shared reference. A struct of slots derives ScenarioState, which adds reset to empty them all.

tests/features/props.feature
Feature: The props cupboard
  Scenario: Borrow a lantern for the rehearsal
    Given the props cupboard is open
    When Marrow borrows the "lantern"
    Then the rehearsal has a lantern
    And the bell is still muffled

  Scenario: Borrow nothing
    Given the props cupboard is open
    Then the rehearsal has nothing borrowed

Compiled and run against rstest-bdd 0.6.0.

tests/props.rs
//! A `#[once]` fixture shared across scenarios, and `ScenarioState`-derived
//! per-scenario state held in `Slot`s.

use std::sync::OnceLock;

use rstest::fixture;
use rstest_bdd::Slot;
use rstest_bdd_macros::{ScenarioState, given, scenario, then, when};

/// Expensive to build and never changed: shared by every scenario.
struct Cupboard {
    shelves: Vec<&'static str>,
}

#[fixture]
#[once]
fn cupboard() -> &'static Cupboard {
    static CUPBOARD: OnceLock<Cupboard> = OnceLock::new();
    CUPBOARD.get_or_init(|| Cupboard {
        shelves: vec!["lantern", "bell muffler", "spare wheel"],
    })
}

/// Fresh for every scenario: what this rehearsal has borrowed.
#[derive(Default, ScenarioState)]
struct Rehearsal {
    borrowed: Slot<&'static str>,
    muffled: Slot<bool>,
}

#[fixture]
fn rehearsal() -> Rehearsal {
    Rehearsal::default()
}

/// Built per scenario from the shared cupboard.
struct Stage {
    cupboard: &'static Cupboard,
}

#[fixture]
fn stage(cupboard: &'static Cupboard) -> Stage {
    Stage { cupboard }
}

#[given("the props cupboard is open")]
fn open(rehearsal: &Rehearsal) {
    rehearsal.muffled.set(true);
}

#[when("Marrow borrows the \"{prop}\"")]
fn borrow(stage: &Stage, rehearsal: &Rehearsal, prop: String) {
    let found = stage
        .cupboard
        .shelves
        .iter()
        .find(|shelf| **shelf == prop)
        .expect("the cupboard has that prop");
    rehearsal.borrowed.set(found);
}

#[then("the rehearsal has a lantern")]
fn has_lantern(rehearsal: &Rehearsal) {
    assert_eq!(rehearsal.borrowed.get(), Some("lantern"));
}

#[then("the bell is still muffled")]
fn muffled(rehearsal: &Rehearsal) {
    assert_eq!(rehearsal.muffled.get(), Some(true));
}

#[then("the rehearsal has nothing borrowed")]
fn nothing(rehearsal: &Rehearsal) {
    assert!(rehearsal.borrowed.is_empty());
}

#[scenario(path = "tests/features/props.feature", name = "Borrow a lantern for the rehearsal")]
fn borrow_a_lantern(stage: Stage, rehearsal: Rehearsal) {
    let _ = (stage, rehearsal);
}

#[scenario(path = "tests/features/props.feature", name = "Borrow nothing")]
fn borrow_nothing(stage: Stage, rehearsal: Rehearsal) {
    let _ = (stage, rehearsal);
}

Compiled and run against rstest-bdd 0.6.0.

Sharing across scenarios

Share infrastructure, not scenario data. The cupboard above is built once, by a #[once] fixture, and never changed; each scenario builds its own Stage and Rehearsal from it. The second scenario states its own starting point rather than relying on whatever the first left behind.

A #[once] fixture is a &'static reference, so steps take it through a per-scenario fixture, as Stage does here, rather than by name.

Fine print