Running and maintaining tests

Version 0.2.0 Updated Dec 08, 2025

Once feature files and step definitions are in place, scenarios run via the usual cargo test command. Test functions created by the #[scenario] macro behave like other rstest tests; they honour #[tokio::test] or #[async_std::test] attributes if applied to the original function. Each scenario runs its steps sequentially in the order defined in the feature file. By default, missing steps emit a compile‑time warning and are checked again at runtime, so steps can live in other crates. Enabling the compile-time-validation feature on rstest-bdd-macros registers steps and performs compile‑time validation, emitting warnings for any that are missing. The strict-compile-time-validation feature builds on this and turns those warnings into compile_error!s when all step definitions are local. This prevents behaviour specifications from silently drifting from the code while still permitting cross‑crate step sharing.

To enable validation, pin a feature in the project's dev-dependencies:

[dev-dependencies]
rstest-bdd-macros = { version = "0.1.0", features = ["compile-time-validation"] }

For strict checking use:

[dev-dependencies]
rstest-bdd-macros = { version = "0.1.0", features = ["strict-compile-time-validation"] }

Steps are only validated when one of these features is enabled.

Best practices for writing effective scenarios include:

  • Keep scenarios focused. Each scenario should test a single behaviour and contain exactly one When step. If multiple actions need to be tested, break them into separate scenarios.

  • Make outcomes observable. Assertions in Then steps should verify externally visible results such as UI messages or API responses, not internal state or database rows.

  • Avoid user interactions in Given steps. Given steps establish context but should not perform actions.

  • Write feature files collaboratively. The value of Gherkin lies in the conversation between the three amigos; ensure that business stakeholders read and contribute to the feature files.

  • Use placeholders for dynamic values. Pattern strings may include format!-style placeholders such as {count:u32}. Type hints narrow the match. Numeric hints support all Rust primitives (u8..u128, i8..i128, usize, isize, f32, f64). Floating-point hints accept integers, decimal forms with optional leading or trailing digits, scientific notation (for example, 1e3, -1E-9), and the special values NaN, inf, and Infinity (matched case-insensitively). Matching is anchored: the entire step text must match the pattern; partial matches do not succeed. Escape literal braces with {{ and }}. Use \ to match a single backslash. A trailing \ or any other backslash escape is treated literally, so \d matches the two-character sequence \d. Nested braces inside placeholders are not supported. Braces are not allowed inside type hints. Placeholders use {name} or {name:type}; the type hint must not contain braces (for example, {n:{u32}} and {n:Vec<{u32}>} are rejected). To describe braces in the surrounding step text (for example, referring to {u32}), escape them as {{ and }} rather than placing them inside {name:type}. The lexer closes the placeholder at the first } after the optional type hint; any characters between the :type and that first } are ignored (for example, {n:u32 extra} parses as name = n, type = u32). name must start with a letter or underscore and may contain letters, digits, or underscores ([A-Za-z_][A-Za-z0-9_]*). Whitespace within the type hint is ignored (for example, {count: u32} and {count:u32} are both accepted), but whitespace is not allowed between the name and the colon. Prefer the compact form {count:u32} in new code. When a pattern contains no placeholders, the step text must match exactly. Unknown type hints are treated as generic placeholders and capture any non-newline text using a non-greedy match.