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.
- 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.
[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.
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.
//! 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
trolleyfixture supplies a freshTrolleyfor each scenario. - A step asks for the fixture by name.
&mut Trolleylets it change the trolley;&Trolleyonly 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.
$ cargo test --test lantern
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:
#[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:
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.
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.
//! 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.
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
The pattern book
Feature files, steps, fixtures, filtering, async, tooling, and the 0.6 upgrade.
New in 0.6Harnesses
Run the same scenario on Tokio, in GPUI, or on a stage of your own.
ReferenceThe full user’s guide
Every feature, organized by the three amigos, in the df12 reference library.