Teams coming from cucumber-rs often ask where the shared World lives in
rstest-bdd. The short answer is: fixtures are the world, and scenario
isolation is the default.
Each #[scenario] expansion produces an ordinary Rust test. rstest builds
fresh fixture values for each test invocation, unless a different lifetime is
opted into, such as #[once]. StepContext is also created per scenario run,
so state mutated in one scenario is not automatically visible in another.
This design keeps scenarios independent, allows parallel execution, and avoids order-dependent failures.
`StepContext::insert_owned` and manual mutable sharing
Most suites should not call StepContext::insert_owned directly. The generated
#[scenario] glue already inserts fixtures for the scenario function and
supports &mut Fixture step parameters.
StepContext::owned_cell creates the type-erased mutable storage required by
insert_owned.
Use insert_owned only when building custom step-execution plumbing outside
the usual macros and registering a mutable fixture manually:
use rstest_bdd::StepContext;
#[derive(Default)]
struct World {
count: usize,
}
let mut ctx = StepContext::default();
let world = StepContext::owned_cell(World::default());
ctx.insert_owned::<World>("world", &world);
Borrowing fixtures from `StepContext`
Custom step-execution plumbing can borrow fixtures through &StepContext.
try_borrow returns an opaque FixtureRef<T> for shared access, while
try_borrow_mut returns an opaque FixtureRefMut<T> for mutable access.
Borrow state is tracked per fixture, so mutable guards for distinct fixtures
can be held concurrently:
use rstest_bdd::{FixtureBorrowError, StepContext};
# fn main() -> Result<(), FixtureBorrowError> {
let mut ctx = StepContext::default();
let counter = StepContext::owned_cell(1_u32);
let label = StepContext::owned_cell(String::from("ready"));
ctx.insert_owned::<u32>("counter", &counter);
ctx.insert_owned::<String>("label", &label);
let mut counter_guard = ctx.try_borrow_mut::<u32>("counter")?;
let mut label_guard = ctx.try_borrow_mut::<String>("label")?;
*counter_guard += 1;
label_guard.push('!');
drop(counter_guard);
drop(label_guard);
assert_eq!(*ctx.try_borrow::<u32>("counter")?, 2);
let original = 3_u64;
ctx.insert("number", &original);
let outcome = ctx.insert_value(Box::new(7_u64));
assert!(outcome.is_inserted());
assert_eq!(*ctx.try_borrow::<u64>("number")?, 7);
assert_eq!(ctx.get::<u64>("number"), Some(&3));
# Ok(())
# }
Step-returned overrides take precedence in try_borrow and try_borrow_mut.
The older get lookup deliberately ignores override storage and reads shared
fixtures only, as the final two assertions show. The borrow_ref and
borrow_mut conveniences expose the same guards as Option; use the try_*
forms when the failure reason matters.
FixtureBorrowError distinguishes NotFound, TypeMismatch,
AlreadyBorrowed, and NotMutable. NotMutable applies when mutable access
is requested for a fixture inserted by shared reference and no step-returned
override is available; conflicting guards for the same fixture produce
AlreadyBorrowed instead.
Migration notes for cucumber-rs users
For migrations from a cucumber World, map the concepts as follows:
Worldstruct: use one or morerstestfixtures.- Mutable world during a scenario: use
&mut FixtureTypein step signatures. - Optional/intermediate values: use
Slot<T>fields in a#[derive(ScenarioState)]fixture. - Global expensive setup: use a narrowly scoped
#[once]fixture returning shared infrastructure, then derive scenario-local state from it.
Antipatterns to avoid
- Depending on scenario execution order (for example "Scenario B uses data from Scenario A") because test ordering is not guaranteed.
- Putting all mutable test data in a process-global static and mutating it from multiple scenarios.
- Using
#[once]for resources that require deterministic teardown inDrop. - Reusing one giant fixture for unrelated concerns instead of composing smaller fixtures.