Skip to content

Getting started · rstest-bdd 0.6.0

One scenario, start to finish.

Add three dev-dependencies, write four lines of Gherkin, bind them to Rust, and run cargo test. Then break it on purpose.

Marrow the felt crab sews a first small patch onto the corner of a blank cream quilt with rust-red wool, while the rabbit holds up a pattern of three pictures and the robot offers a spool of thread.
The first patch Marrow sews the first patch; Clover holds the pattern and Bobbin brings the thread.
Rust
Stable, 1.88 or newer
rstest
0.26.1+
rstest-bdd
0.6.0, released 14 September 2026
Installs
Nothing globally

Add the crates

Scenarios are tests, so the crates are dev-dependencies. The step and scenario macros are published separately from the runtime, in rstest-bdd-macros, and a test imports them from rstest_bdd_macros.

Cargo.toml
[dev-dependencies]
rstest = "0.26.1"
rstest-bdd = "0.6.0"
rstest-bdd-macros = "0.6.0"

Compiled and run against rstest-bdd 0.6.0.

Write the feature

A feature file is plain text. One Scenario states one behaviour: where it starts, what happens, and what should be true afterwards. The file below is the whole of it.

tests/features/lantern.feature
Feature: Lantern delivery
  Scenario: Deliver a lantern
    Given a lantern on the trolley
    When the departure bell rings
    Then the lantern arrives upright

Compiled and run against rstest-bdd 0.6.0.

The #[scenario] attribute names this file by a path relative to the crate root, and reads it when the test compiles. Editing the feature file triggers a rebuild.

Write the steps

Each line of the scenario needs a function whose attribute carries the same words. The test file below binds all three, and the scenario itself.

tests/lantern.rs
//! One scenario bound to three steps, which share a fixture and change it
//! through `&mut`.

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

#[derive(Default)]
struct Trolley {
    loaded: bool,
    arrived: bool,
    upright: bool,
}

#[fixture]
fn trolley() -> Trolley {
    Trolley::default()
}

#[given("a lantern on the trolley")]
fn load(trolley: &mut Trolley) {
    trolley.loaded = true;
    trolley.upright = true;
}

#[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);
}

#[scenario(path = "tests/features/lantern.feature", name = "Deliver a lantern")]
fn lantern_delivery(trolley: Trolley) {
    let _ = trolley;
}

Compiled and run against rstest-bdd 0.6.0.

  • The trolley fixture supplies a fresh Trolley for each scenario.
  • A step asks for the fixture by name. &mut Trolley lets it change the trolley; &Trolley only reads it.
  • #[scenario] names the file and the scenario. The function becomes the test, and its body runs after the last step, with the same trolley.

Run it

The scenario is an ordinary test named after the function, so cargo test runs it and a filter selects it.

Terminal
$ cargo test --test lantern
Outputrstest-bdd 0.6.0
running 1 test
test lantern_delivery ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Break the promise

A test earns its keep when it fails. Tip the lantern over in the When step by adding one line to depart:

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

The added line is the last one in the function.

The run now fails, and says where:

Outputfailed
running 1 test
test lantern_delivery ... FAILED

failures:

---- lantern_delivery stdout ----

thread 'lantern_delivery' (726627) panicked at tests/lantern.rs:32:5:
assertion failed: trolley.upright
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'lantern_delivery' (726627) panicked at tests/lantern.rs:35:1:
Step failed at index 2: Then the lantern arrives upright - Panic in step 'the lantern arrives upright', function 'check': assertion failed: trolley.upright (feature: tests/features/lantern.feature, scenario: Deliver a lantern)


failures:
    lantern_delivery

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Repeat with examples

One lantern is a start. A Scenario Outline runs the same scenario once per row of its Examples table, and each <count> is replaced by the row’s value before the steps are matched.

tests/features/counting.feature
Feature: Counting lanterns
  Scenario Outline: Deliver <count> lanterns
    Given a trolley carrying <count> lanterns
    When the departure bell rings
    Then <count> lanterns arrive at the picnic

    Examples:
      | count |
      | 1     |
      | 2     |
      | 0     |

Compiled and run against rstest-bdd 0.6.0.

The steps read the count with a typed placeholder, {count:u32}, which parses the text through FromStr before the step runs. The scenario function receives each row’s column as a String.

tests/counting.rs
//! A Scenario Outline: one test generated per Examples row, with the
//! placeholder value threaded into the test function as a parameter.

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

#[derive(Default)]
struct Trolley {
    lanterns: u32,
    delivered: u32,
}

#[fixture]
fn trolley() -> Trolley {
    Trolley::default()
}

#[given("a trolley carrying {count:u32} lanterns")]
fn load(trolley: &mut Trolley, count: u32) {
    trolley.lanterns = count;
}

#[when("the departure bell rings")]
fn depart(trolley: &mut Trolley) {
    trolley.delivered = trolley.lanterns;
}

#[then("{count:u32} lanterns arrive at the picnic")]
fn arrive(trolley: &Trolley, count: u32) {
    assert_eq!(trolley.delivered, count);
}

#[scenario(path = "tests/features/counting.feature")]
fn counting_lanterns(trolley: Trolley, count: String) {
    let _ = (trolley, count);
}

Compiled and run against rstest-bdd 0.6.0.

cargo test --test countingrstest-bdd 0.6.0
running 3 tests
test counting_lanterns::case_2 ... ok
test counting_lanterns::case_1 ... ok
test counting_lanterns::case_3 ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Three rows, three tests, reported separately. The zero row is not an afterthought: an empty trolley that acquires a lantern on the way is a bug too.

Where next