Skip to content

Behaviour-driven development · Rust

Small stories. Real Rust tests.

Agree what should happen. Write it down. Put it to the test.

rstest-bdd connects plain-language Gherkin scenarios to Rust functions and rstest fixtures. Every scenario becomes an ordinary test, and cargo test runs it beside the rest.

  • rstest-bdd 0.6.0
  • Stable Rust 1.88+
  • rstest 0.26.1+
  • ISC licence
A needle-felted picnic at dusk on a patchwork landscape: a cream rabbit pours tea, Marrow the round orange crab holds a shortbread biscuit, and a blue-grey sewn robot unfolds a napkin, beside a trolley of glowing lanterns and a banner of three pictures.
The promise The lanterns arrived. The kettle was on.

The lantern picnic

A promise worth getting right.

Clover plans a picnic. Bobbin builds a lantern trolley. Marrow turns their agreement into something all three can check.

  • Clover owns the promise — the product owner, who writes and reviews the feature file
  • Bobbin builds the mechanism — the developer, who writes the steps and fixtures
  • Marrow checks what arrived — the tester, who runs the scenarios and reads the failures
Marrow the felt crab, a cream rabbit, and a cloth robot lean over a workshop table to agree a three-picture storyboard: a trolley with a lantern, a bell, and an upright lantern.
Chapter 01 Marrow aligns the storyboard. Clover points to the promise; Bobbin brings the trolley.

Chapter 01 · Shared understanding

First, agree what good looks like.

A lantern on the trolley. A ring of the bell. An upright lantern at the picnic table. Three pictures make the promise concrete enough to argue about.

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

Marrow clips a storyboard above a toy trolley route, with a bell and a release mechanism along the track.
Chapter 02 Marrow attaches the storyboard; Bobbin connects the mechanism and Clover checks the destination.

Chapter 02 · Executable steps

A story with something attached.

Marrow clips the pictures above the route. Bobbin has already built the mechanism. Clover holds the promised result beside the destination.

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

Compiled and run against rstest-bdd 0.6.0.

A workshop bench holds a wheel jig, a bell check, and the full trolley rehearsal side by side.
Chapter 03 Marrow adds the rehearsal to the bench while Bobbin checks the bell and Clover brings a stool.

Chapter 03 · The existing test runner

A new story. The same workshop.

A wheel check, a bell check, and a full rehearsal share the familiar bench. Clover brings another stool. Nobody builds an extension.

Terminal
$ cargo test lantern_delivery
A compartmented props cupboard supplies separate trolley rehearsal boards for Marrow and the robot.
Chapter 04 Marrow and Bobbin prepare separate rehearsals; Clover brings a lantern from the props cupboard.

Chapter 04 · Composable fixtures

Good beginnings bear repeating.

Marrow opens the props cupboard. Every rehearsal gets its own freshly reset trolley. The bells have their own muffler.

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

Compiled and run against rstest-bdd 0.6.0.

Three miniature lanes carry exactly one lantern, two lanterns, and no lanterns.
Chapter 05 Marrow inspects the empty trolley while Bobbin and Clover rehearse one-, two-, and zero-lantern deliveries.

Chapter 05 · Explicit example cases

Nothing was a delivery too.

Once with one lantern. Once with two. Once with none. Marrow gives the empty trolley their full attention. It must not acquire a lantern on the way.

tests/features/counting.feature
Examples:
  | count |
  | 1     |
  | 2     |
  | 0     |

Compiled and run against rstest-bdd 0.6.0.

Marrow holds a picture of an upright lantern beside the real lantern, which glows while lying on its side in the trolley.
Chapter 06 Marrow compares the promise with the fallen lantern; Clover and Bobbin investigate the strap.

Chapter 06 · Useful failures

The trolley arrived. The promise didn’t.

The lantern still glows. It also lies on its side. Marrow holds up the original picture. Nearly right is useful information.

cargo testReal output
assertion failed: trolley.upright
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)
Marrow, the rabbit, and the robot carry the repaired trolley towards timber, clockwork, and window-shaped stage supports.
Chapter 07 Marrow, Clover, and Bobbin choose a stage for the repaired trolley.

Chapter 07 · Harness adapters

Keep the story. Choose its stage.

The strap is fixed. The three carry the trolley towards another stage, ready for a fresh rehearsal.

tests/stage.rs
harness = rstest_bdd_harness_tokio::TokioHarness,

Compiled and run against rstest-bdd 0.6.0.

At dusk, Marrow, the rabbit, and the cloth robot share tea and shortbread on a checked blanket beside the trolley of glowing lanterns.
Chapter 08 Marrow settles down with shortbread while Clover pours tea and Bobbin lays out a napkin.

Chapter 08 · A promise kept

And then there was time for tea.

The lanterns stand upright. Clover pours, Bobbin unfolds a napkin, and Marrow puts the clipboard down. One piece of shortbread remains unaccounted for.

cargo testReal output
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

From the story to the test

Readable on paper. Runnable in Rust.

A feature file describes the behaviour. Attribute macros connect its lines to ordinary functions. One attribute binds the two into a test.

Start here

Add the test dependencies.

Three dev-dependencies. The macros live in their own crate, so 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.

Describe a delivery.

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 states an observable result. Rust supplies the implementation and the assertions.

Use the usual runner.

Terminal
$ cargo test lantern_delivery

The scenario runs as lantern_delivery, beside the unit and integration tests. Outline rows run as case_1, case_2, and so on.

Connect the Rust steps.

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.

A small in-memory model. In an application’s tests the steps call the real delivery code. Set trolley.upright = false; in depart to watch the promise fail.

Notes from the workbench

Small parts. Useful together.

BDD works when the examples clarify the domain. The plumbing should get out of the way of that conversation.

Reuse the preparation.

Unit, integration, and scenario tests draw on the same rstest fixtures. State lives where rstest puts it, not in a framework-mandated World. Reusing a fixture does not mean sharing a mutable one.

Fixtures and state

Make the examples explicit.

Placeholders such as {count:u32} parse through FromStr before the step runs. An Outline repeats a scenario for each row of its Examples table.

Define steps

Find the useful mismatch.

Ordinary assertions check the result. A failure names the step, the function, the feature, and the scenario, so the broken promise is one read away.

Bind and filter scenarios
The rabbit, Marrow, and the robot push a toy trolley with an upright glowing lantern across a patchwork quilt towards three felt puppet-theatre stages: oatmeal wool, mustard with brass cogs, and night blue with embroidered stars.
The same rehearsal Marrow, Clover, and Bobbin take the repaired trolley to a new stage.

The same rehearsal, a different stage

Choose where the scenario runs.

Harness adapters supply the environment around a scenario. 0.6.0 ships a standard harness, a Tokio harness, and a GPUI harness, and the interface to write another.

The application code still has to suit the stage it is given. A new stage is not a portability spell.

How harnesses work

The honest small print

Know what the checks promise.

Does the compiler catch a missing step?

Not by default. A missing step fails when its scenario runs, and the failure names the step, the feature, and the scenario. Enable rstest-bdd-macros/strict-compile-time-validation and the same mistake stops the build, provided every step the scenario uses is defined in the crate being compiled.

Strict validation in the tooling guide

When is rstest-bdd a good fit?

When a team wants readable acceptance scenarios and wants to keep rstest fixtures and cargo test. cucumber-rs offers its own runner and a World model; the right choice depends on the suite.

rstest-bdd and cucumber-rs, side by side

What can a feature file express?

Background, Scenario Outlines with Examples, data tables, docstrings, tags, and keywords in any language Gherkin supports. The repository runs a Japanese household ledger end to end.

Write feature files

What are the boundaries?

Feature files are read and step definitions registered at compile time, so there is no dynamic step registration. The * step keyword is not matched. Async scenarios run on Tokio’s current-thread runtime.

Limitations in the user’s guide

4 · Time for tea

The promise held. On with the picnic.

Start with one behaviour that matters. Give it a readable example and a real assertion.

Marrow has put the clipboard down. Please respect this development.