The #[scenario] macro is the entry point that ties a Rust test function to a
scenario defined in a .feature file. It accepts four arguments:
| Argument | Purpose | Status |
|---|---|---|
path: &str |
Relative path to the feature file (required). | Implemented: resolved and parsed at compile time. |
index: usize |
Optional zero-based scenario index (defaults to 0). |
Implemented: selects the scenario by position. |
name: &str |
Optional scenario title; resolves when unique. | Implemented: errors when missing and directs duplicates to index. |
tags: &str |
Optional tag-expression filter applied at expansion. | Implemented: filters scenarios and outline example rows; errors when nothing matches. |
Tag filters run at macro-expansion time against the union of tags on the
feature, the matched scenario, and—when dealing with Scenario Outline—the
Examples: block that produced each generated row. Expressions support the
operators and, or, and not (case-insensitive) and respect the precedence
not > and > or; parentheses may be used to override this ordering. Tags
include the leading @. When the filter is combined with index or name,
the macro emits a compile error if the selected scenario does not satisfy the
expression.
# use rstest_bdd_macros::scenario;
#[scenario(path = "features/search.feature", tags = "@fast and not @wip")]
fn smoke_test() {}
If the feature file cannot be found or contains invalid Gherkin, the macro emits a compile-time error with the offending path.
When name is provided, the macro matches the title case-sensitively. A
missing title triggers a diagnostic listing the available headings in the
feature. Duplicate titles yield a diagnostic that highlights the conflict and
lists the matching indexes and line numbers, enabling selection by the index
argument when required.
During macro expansion, the feature file is read and parsed. The macro
generates a new test function annotated with #[rstest::rstest] that performs
the following steps:
-
Build a
StepContextand insert the test’s fixture arguments into it. -
For each step in the scenario (according to the
Given‑When‑Thensequence), look up a matching step function by(keyword, pattern)in the registry. A missing step causes the macro to emit a compile‑time error such asNo matching step definition found for: Given an undefined step, allowing detection of incomplete implementations before tests run. Multiple matching definitions likewise produce an error. -
Invoke the registered step function with the
StepContextso that fixtures are available inside the step. -
After executing all steps, run the original test body. This block can include extra assertions or cleanup logic beyond the behaviour described in the feature.
Because the generated code uses #[rstest::rstest], it integrates seamlessly
with rstest features such as parameterized tests and asynchronous fixtures.
Tests are still discovered and executed by the standard Rust test runner, so
one may filter or run them in parallel as usual.
Skipping scenarios
Steps or hooks may call rstest_bdd::skip! to stop executing the remaining
steps. The macro records a Skipped outcome and short-circuits the scenario so
the generated test returns before evaluating the annotated function body.
Invoke skip!() with no arguments to record a skipped outcome without a
message. Pass an optional string to describe the reason, and use the standard
format! syntax to interpolate values when needed. Set the
RSTEST_BDD_FAIL_ON_SKIPPED environment variable to 1, or call
rstest_bdd::config::set_fail_on_skipped(true), to escalate skipped scenarios
into test failures unless the feature or scenario carries an @allow_skipped
tag. (Example-level tags are not yet evaluated.)
The macro captures the current execution scope internally, so helper functions
may freely call skip! as long as they eventually run within a step or hook.
When code outside that context—for example, a unit test or module
initialization routine—invokes the macro, it panics with the message
rstest_bdd::skip! may only be used inside a step or hook generated by rstest-bdd.
Each scope tracks the thread that entered it; issuing a skip from another
thread panics with
rstest_bdd::skip! may only run on the thread executing the step .... Keep
skip requests on the thread that executes steps, so the runner can intercept
the panic payload.
# use rstest_bdd as bdd;
# use rstest_bdd_macros::{given, scenario};
#[given("a dependent service is unavailable")]
fn service_unavailable() {
bdd::skip!("service still provisioning");
}
#[given("a maintenance window is scheduled")]
fn maintenance_window() {
let component = "billing";
bdd::skip!(
"{component} maintenance in progress",
component = component,
);
}
#[scenario(path = "features/unhappy_path.feature")]
fn resilience_test() {
panic!("scenario body is skipped");
}
When fail_on_skipped is enabled, apply @allow_skipped to the feature or the
scenario to document that the skip is expected:
@allow_skipped
Scenario: Behaviour pending external contract
Given a dependent service is unavailable
Asserting skipped outcomes
Tests that exercise skip-heavy flows no longer need to match on enums to verify
that a step or scenario stopped executing. Use
rstest_bdd::assert_step_skipped! to unwrap a StepExecution::Skipped
outcome, optionally constraining its message, and
rstest_bdd::assert_scenario_skipped! to inspect
ScenarioStatus records. Both macros
accept message_absent = true to assert that no message was provided and
substring matching to confirm that a message contains the expected reason.
use rstest_bdd::{assert_scenario_skipped, assert_step_skipped, StepExecution};
use rstest_bdd::reporting::{ScenarioRecord, ScenarioStatus, SkippedScenario};
let outcome = StepExecution::skipped(Some("maintenance pending".into()));
let message = assert_step_skipped!(outcome, message = "maintenance");
assert_eq!(message, Some("maintenance pending".into()));
let record = ScenarioRecord::new(
"features/unhappy.feature",
"pending work",
ScenarioStatus::Skipped(SkippedScenario::new(None, true, false)),
);
let details = assert_scenario_skipped!(
record.status(),
message_absent = true,
allow_skipped = true,
forced_failure = false,
);
assert!(details.allow_skipped());