rstest-bdd 0.5.0

A BDD testing framework for Rust, built on rstest.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# rstest-bdd

*Where Rustaceans come for their gourd‑related puns.*

> **TL;DR**: Behaviour‑Driven Development (BDD), idiomatic to Rust. Keep your
> unit tests and your acceptance tests on the same vine, run everything with
> `cargo test`, and reuse your `rstest` fixtures.

______________________________________________________________________

## Why this crate?

`rstest-bdd` brings the collaborative clarity of BDD to Rust **without** asking
you to adopt a bespoke runner or a monolithic “world” object. Instead, it
builds on the excellent `rstest` fixture and parametrisation model:

- **One runner to rule them all**: execute scenarios with `cargo test`.

- **First‑class fixtures**: share `rstest` fixtures between unit, integration,
  and BDD tests.

- **Ergonomic step definitions**: `#[given]`, `#[when]`, `#[then]` with typed
  placeholders.

- **Feature parity**: Scenario Outlines, Background, data tables, and
  docstrings.

- **Pytest‑bdd vibes**: explicit `#[scenario]` binding from test code to a
  named scenario.

  The attribute now requires a `path` argument pointing to the `.feature` file;
  index-only usage is no longer supported.

  Migration (since 0.1.0-alpha2):

  ```rust,no_run
  // Before
  #[scenario(index = 0)]
  // After
  #[scenario(path = "tests/features/example.feature", index = 0)]
  ```

Think of it as *courgette‑driven* development: crisp, versatile, and it plays
nicely with everything else on your plate.

______________________________________________________________________

## Installation

Add the crates to your **dev‑dependencies**:

```toml
# Cargo.toml
[dev-dependencies]
rstest = "0.26.1"
rstest-bdd = "0.5.0"
```

Feature flags:

- `tokio` / `async-std` — choose your async test attribute (`#[tokio::test]`,
  etc.).

- `no-inventory` — fallback code‑gen registry for platforms where
  linker‑section collection is unwieldy.

- `compile-time-validation` — registers steps at compile time and reports
  missing or ambiguous steps with spans. (Disabled by default.)

- `strict-compile-time-validation` — fails compilation when steps are missing
  or ambiguous; implies `compile-time-validation`. (Disabled by default.)

Both features are disabled by default and apply only to the `rstest-bdd-macros`
crate. Enable them in your `Cargo.toml` with:

```toml
[dependencies]
rstest-bdd-macros = { version = "0.5.0", features = ["compile-time-validation"] }
```

Or via CLI:

```bash
cargo test --features "rstest-bdd-macros/compile-time-validation"
cargo test --features "rstest-bdd-macros/strict-compile-time-validation"
```

______________________________________________________________________

## Quick start (end‑to‑end “Web search”)

**Feature file**: `tests/features/web_search.feature`

```gherkin
Feature: Web Search
  As a user, I want to search for information,
  so that I can find what I'm looking for.

  Scenario: Simple web search
    Given the DuckDuckGo home page is displayed
    When I search for "Rust programming language"
    Then the search results page is displayed
    And the results contain "Rust Programming Language"
```

**Step definitions**: `tests/test_web_search.rs`

```rust,no_run
use rstest::fixture;
use rstest_bdd::{scenario, given, when, then, StepResult};
// Browser automation example — pick your favourite WebDriver crate.
use thirtyfour::prelude::*;

#[fixture]
async fn browser() -> WebDriverResult<WebDriver> {
    let caps = DesiredCapabilities::firefox();
    Ok(WebDriver::new("http://localhost:4444", caps).await?)
}

// Bind this test to the named scenario from the feature file.
// The test body runs *after* all steps have passed.
#[scenario(path = "tests/features/web_search.feature", name = "Simple web search")]
#[tokio::test]
async fn test_simple_search(#[future] browser: WebDriver) {
    // Optional: final assertions / cleanup that aren't natural Gherkin steps.
}

#[given("the DuckDuckGo home page is displayed")]
async fn go_to_home(browser: &mut WebDriver) -> StepResult<(), thirtyfour::error::WebDriverError> {
    browser.goto("https://duckduckgo.com/").await?;
    Ok(())
}

#[when("I search for \"(.*)\"")]
async fn search_for_phrase(
    browser: &mut WebDriver,
    phrase: String,
) -> StepResult<(), thirtyfour::error::WebDriverError> {
    let form = browser.find(By::Id("search_form_input_homepage")).await?;
    form.send_keys(&phrase).await?;
    form.submit().await?;
    Ok(())
}

#[then("the search results page is displayed")]
async fn results_page_is_displayed(
    browser: &mut WebDriver,
) -> StepResult<(), thirtyfour::error::WebDriverError> {
    browser.find(By::Id("links")).await?;
    Ok(())
}

#[then("the results contain \"(.*)\"")]
async fn results_contain_text(
    browser: &mut WebDriver,
    text: String,
) -> StepResult<(), thirtyfour::error::WebDriverError> {
    let content = browser.source().await?;
    if content.contains(&text) { Ok(()) }
    else { Err(thirtyfour::error::WebDriverError::CustomError(
        format!("Result text not found: expected substring '{text}'")
    )) }
}
```

Run it:

```bash
cargo test -p your-crate -- --nocapture
```

Everything grows on the same trellis: your fixtures, your filters
(`cargo test search`), and your parallelism all continue to work as usual.

## Internationalisation in practice

Feature files can opt into another language by adding `# language: <code>` to
the first line. The Gherkin parser loads the appropriate keyword catalogue so
that teams can keep authoring steps in their preferred language. The
`examples/japanese-ledger` crate shows the full workflow in Japanese, including
Unicode step patterns, and a household ledger domain. Run it with
`cargo test -p japanese-ledger` to see two Japanese scenarios execute end to
end.

______________________________________________________________________

## Step definitions 101

Decorate plain Rust functions:

```rust,no_run
use rstest_bdd::{given, when, then};

#[given("an empty basket")]
fn empty_basket(basket: &mut Basket) {
    basket.clear();
}

#[when("I add {count:u32} pumpkins")]
fn add_pumpkins(basket: &mut Basket, count: u32) {
    basket.add(Item::Pumpkin, count);
}

#[then("the basket has {count:u32} pumpkins")]
fn assert_count(basket: &Basket, count: u32) {
    assert_eq!(basket.count(Item::Pumpkin), count);
}
```

Implicit fixtures such as `basket` must already be in scope in the test module;
`#[from(name)]` only renames a fixture and does not create one.

- Patterns accept **typed placeholders** like `{count:u32}`; values parse via
  `FromStr`.

- Fixtures are injected automatically when parameter names match fixtures;
  use `#[from(name)]` only to rename a parameter.

- Prefer readable step text first; compile‑time checks ensure you don't forget
  an implementation.

### Manual async wrappers for sync steps

When an explicit async wrapper is required around an existing synchronous step
handler, use `rstest_bdd::async_step::sync_to_async`. In parameter positions,
`StepContext<'_>` lets Rust infer the fixture lifetime, leaving only the borrow
lifetime to name:

```rust,no_run
use rstest_bdd::async_step::sync_to_async;
use rstest_bdd::{StepContext, StepError, StepExecution, StepFuture};

fn sync_step(
    _ctx: &mut StepContext<'_>,
    _text: &str,
    _docstring: Option<&str>,
    _table: Option<&[&[&str]]>,
) -> Result<StepExecution, StepError> {
    Ok(StepExecution::from_value(None))
}

fn async_wrapper<'ctx>(
    ctx: &'ctx mut StepContext<'_>,
    text: &'ctx str,
    docstring: Option<&'ctx str>,
    table: Option<&'ctx [&'ctx [&'ctx str]]>,
) -> StepFuture<'ctx> {
    sync_to_async(sync_step)(ctx, text, docstring, table)
}
```

`rstest_bdd` also exports compact aliases (`StepCtx`, `StepTextRef`, `StepDoc`,
`StepTable`) for these wrapper signatures.

```rust,no_run
use rstest_bdd::async_step::sync_to_async;
use rstest_bdd::{StepCtx, StepDoc, StepFuture, StepTable, StepTextRef};

fn async_wrapper_with_aliases<'ctx>(
    ctx: StepCtx<'ctx, '_>,
    text: StepTextRef<'ctx>,
    docstring: StepDoc<'ctx>,
    table: StepTable<'ctx>,
) -> StepFuture<'ctx> {
    sync_to_async(sync_step)(ctx, text, docstring, table)
}
```

### cucumber-rs migration

For easier migration from cucumber-rs, step macros also accept the
`expr = "..."` syntax:

```rust,no_run
use rstest_bdd::{given, when, then};

// cucumber-rs style (supported for migration):
#[given(expr = "an empty basket")]
fn empty_basket(basket: &mut Basket) {
    basket.clear();
}

// rstest-bdd style (preferred for new code):
#[given("an empty basket")]
fn empty_basket_alt(basket: &mut Basket) {
    basket.clear();
}
```

Both forms are functionally equivalent. The direct string literal syntax is
recommended for new code as it is more concise.

______________________________________________________________________

## Scenario outline ≈ parametrised tests

Write once, test many:

**Feature**:

```gherkin
Scenario Outline: Login with different credentials
  Given I am on the login page
  When I enter username "<username>" and password "<password>"
  Then I should see the message "<message>"

  Examples:
    | username | password   | message                 |
    | user     | correctpass| Welcome, user!         |
    | user     | wrongpass  | Invalid credentials     |
    | admin    | adminpass  | Welcome, administrator! |
```

**Test**:

```rust,no_run
#[scenario(path = "tests/features/login.feature", name = "Login with different credentials")]
#[tokio::test]
async fn test_login_scenarios(#[future] browser: WebDriver) {}

// Use typed placeholders to bind arguments by name.
#[when("I enter username {username} and password {password}")]
async fn enter_credentials(
    browser: &mut WebDriver,
    username: String,
    password: String,
) {
    // ...
}

#[then("I should see the message {message}")]
async fn see_message(browser: &mut WebDriver, message: String) {
    // ...
}
```

Under the rind, `#[scenario]` expands to an `rstest` parametrised test, so
cases show up individually in your runner.

______________________________________________________________________

## Background, tables, and docstrings

- **Background** runs before every scenario in the feature.
- **Data tables** arrive in a `datatable` parameter whose type implements
  `TryFrom<Vec<Vec<String>>>`. Continue using `Vec<Vec<String>>` or upgrade to
  `rstest_bdd::datatable::Rows<T>` for typed parsing.

- **Docstrings** arrive as a `String`.

```rust,no_run
#[given("the following users exist:")]
fn create_users(
    db: &mut DbConnection,
    #[datatable] datatable: rstest_bdd::datatable::Rows<UserRow>,
) {
    // `UserRow` implements `datatable::DataTableRow` elsewhere in the module.
    for row in datatable {
        db.insert_user(&row.name, &row.email);
    }
}
```

______________________________________________________________________

## Internationalization and localization

Write feature files in any language supported by Gherkin. Declare the locale at
the top of the `.feature` file and keep using the usual step macros:

```gherkin
# language: fr
Fonctionnalité: Panier
  Scénario: Ajouter un article
    Étant donné un panier vide
    Quand l'utilisateur ajoute une citrouille
    Alors le panier contient une citrouille
```

Keyword parsing is delegated to the `gherkin` crate, so `#[given]`, `#[when]`
and `#[then]` continue to match the translated keywords without additional
configuration.

Runtime diagnostics ship as Fluent translations bundled with the crate. English
messages are always available; call `select_localizations` to request another
locale before running scenarios:

```rust,no_run
use rstest_bdd::select_localizations;
use unic_langid::langid;

select_localizations(&[langid!("es")])?; // Switch diagnostics to Spanish
```

Missing translations fall back to English, and procedural macro diagnostics
remain in English so builds stay deterministic across environments.

______________________________________________________________________

## How it works (the short tour)

- **Attribute macros**:

  - `#[given] / #[when] / #[then]` register step metadata.

  - `#[scenario]` binds a test function to a named scenario in a `.feature`
    file.

- **Discovery**: Steps are registered at compile time into a global registry
  (via linker‑section collection). At runtime, the generated test matches each
  Gherkin line against that registry and invokes the correct function.

- **Safety rails**: If a step in the feature has no matching implementation,
  you get a **compile error** with a helpful message, not a late test failure.

- **Fixtures**: Because the generated test is an `rstest`, your fixture
  dependency graph Just Works™.

If your target platform dislikes linker sections, enable the `no-inventory`
feature to switch to a build‑script registry.

______________________________________________________________________

## Design principles

- **Ecosystem, not empire**: reuse `rstest` instead of replacing it.

- **Readable first**: human‑centric step text, type‑safe argument parsing.

- **Fail fast**: resolve missing steps at compile time.

- **Zero new runners**: keep Continuous Integration (CI) / Continuous Delivery
  (CD) and IDE behaviour unchanged.

______________________________________________________________________

## Limitations

- `.feature` files are processed at compile time; scenarios are static.

- Step definitions must be known at compile time (no dynamic registration).

- IDE navigation from Gherkin to Rust may require tooling support.

- Registry implementation relies on platform features; use `no-inventory` if
  needed.

______________________________________________________________________

## [Roadmap]docs/roadmap.md

1. **Core mechanics**: step registry, `#[scenario]`, exact matching (done/PoC).

2. **Fixtures & parametrisation**: typed placeholders, Scenario Outline →
   `#[case]`.

3. **Feature parity & ergonomics**: Background, tables, docstrings,
   `scenarios!` macro, richer diagnostics.

4. **Developer tools**: `cargo bdd list-steps`, editor integrations.

We’re not here to replace `cucumber`; we’re here to offer a different trade‑off
for teams already invested in `rstest` and `cargo test`.

______________________________________________________________________

## Comparison at a glance

| Feature          | `rstest-bdd` (proposed)                     | `cucumber` (Rust)                 |
| ---------------- | ------------------------------------------- | --------------------------------- |
| Test runner      | `cargo test` (`rstest` under the hood)      | Custom runner (`World::run(...)`) |
| State management | `rstest` fixtures                           | `World` struct                    |
| Step discovery   | Compile‑time registration + runtime match   | Runner‑driven collection          |
| Scenario Outline | Maps to `rstest` parametrisation            | Built into runner                 |
| Async            | Runtime‑agnostic via features               | Built‑in with specified runtime   |
| Philosophy       | BDD as an **extension** of `rstest`         | Rust port of classic Cucumber     |

______________________________________________________________________

## Workspace layout

```text
rstest-bdd/             # Runtime crate (re-exports macros for convenience)
rstest-bdd-macros/      # Procedural macro crate
rstest-bdd-policy/      # Shared execution policy enums
```

## Examples

An `examples` directory hosts standalone crates demonstrating `rstest-bdd`.
These crates are members of the repository's root workspace, so CI and
workspace commands include them. Each example can also be built and tested from
its directory. To build and test the `todo-cli` example:

```bash
cd examples/todo-cli
cargo test
# NOTE: The CLI stores tasks only in memory per invocation. Each `cargo run`
# starts with an empty list, so the 'list' command below will be empty.
cargo run -- add "Buy milk"   # adds in this process, then exits
cargo run -- list             # runs in a new process; prints an empty list
```

Dependencies for examples are captured in the repository's `Cargo.lock` to
ensure reproducible builds.

Note: `make nixie` renders Mermaid diagrams via `@mermaid-js/mermaid-cli`.
Ensure a supported runner is available (listed in preferred order):

- Bun: `bun x @mermaid-js/mermaid-cli`
- pnpm: `pnpm dlx @mermaid-js/mermaid-cli`
- Node.js: `npx --yes @mermaid-js/mermaid-cli`

If none is installed, install one, and re-run `make nixie`.

______________________________________________________________________

## Prior art & acknowledgements

- Inspired by the ergonomics of **pytest‑bdd** and the fixture model of
  **rstest**.

- Uses a global step registry pattern popularised in Rust by the **inventory**
  crate.

- Tips the hat to **cucumber‑rs** for bringing Cucumber’s ideas to Rust.

______________________________________________________________________

## Contributing

Issues, ideas, and PRs are very welcome. Please include:

- A minimal repro (feature file + steps) when filing bugs.

- Before/after compiler output if you hit macro errors (the more precise, the
  better).

- Platform info if you suspect a registry/linker quirk.

Let’s **seed** a lovely ecosystem together.

______________________________________________________________________

## Licence

ISC Licence — because that’s how we roll. You’re free to use, copy, modify, and
distribute this software for any purpose, with or without fee, and provided
that the copyright notice and this permission notice are included in all
copies. The software is provided “as is”, without warranty of any kind. See
`LICENSE` for the full text.

______________________________________________________________________

## Appendix: FAQ

**Does it work with stable Rust?**\
Yes; nothing here requires nightly.

**Can I mix BDD tests with unit tests in the same crate?**\
Absolutely. They run under the same `cargo test` umbrella and can share
fixtures.

**Will it slow my build?**\
There’s some compile‑time I/O to parse `.feature` files. For large suites,
caching parsed ASTs in `OUT_DIR` mitigates this (built in).

**Do I *have* to use regexes in step patterns?**\
No. Prefer typed placeholders like `{n:u32}`; fall back to regex groups only
when you really need them.

______________________________________________________________________

Happy testing — and may your scenarios be **gourd‑geous** and your failures
easy to **squash**. 🎃🧪🦀