diff --git a/docs/behavioural-testing-in-rust-with-cucumber.md b/docs/behavioural-testing-in-rust-with-cucumber.md
index d5a18b2a..cf6c6b4d 100644
--- a/docs/behavioural-testing-in-rust-with-cucumber.md
+++ b/docs/behavioural-testing-in-rust-with-cucumber.md
@@ -4,82 +4,84 @@
Behaviour-Driven Development (BDD) is a software development process that
evolved from Test-Driven Development (TDD). Although testing remains integral,
-the primary focus is on collaboration and communication among developers,
-QA teams, business analysts, and product owners. This guide walks through
+the primary focus is on collaboration and communication among developers, QA
+teams, business analysts, and product owners. This guide walks through
implementing BDD in Rust with the modern `cucumber` testing framework, covering
practical techniques, best practices, and lessons from real-world projects.
### 1.1 Beyond Testing: BDD as a Collaborative Process
At its core, BDD is not merely a testing technique but a methodology for
-building a shared understanding of a system's behaviour.1 The central goal is to
-create a ubiquitous language that both technical and non-technical stakeholders
-can use to describe and agree upon software requirements.3 This process is
-centred on conversation; the discussions about how a feature should behave are
-the most valuable output of BDD.2
+building a shared understanding of a system's behaviour.[^1] The central goal
+is to create a ubiquitous language that both technical and non-technical
+stakeholders can use to describe and agree upon software requirements.[^2] This
+process is centred on conversation; the discussions about how a feature should
+behave are the most valuable output of BDD.[^3]
-The tangible artifact of these conversations is a set of specifications written
+The tangible artefact of these conversations is a set of specifications written
in a structured, natural language format. These specifications serve a dual
purpose: they are human-readable documentation of the system's features, and
-they are also executable tests that verify the system's behaviour. This approach
-ensures that documentation and implementation cannot drift apart, creating a
-suite of "living documentation."
+they are also executable tests that verify the system's behaviour. This
+approach ensures that documentation and implementation cannot drift apart,
+creating a suite of "living documentation."
The value of BDD is realized before a single line of implementation code is
written. When a development team writes behaviour specifications in isolation,
they are simply using a different syntax for their tests. The transformative
-potential of BDD is unlocked only when these specifications are co-created
-and validated through a collaborative process involving all team members.
-This ensures that what is built is precisely what the business needs, reducing
+potential of BDD is unlocked only when these specifications are co-created and
+validated through a collaborative process involving all team members. This
+ensures that what is built is precisely what the business needs, reducing
ambiguity and rework.
### 1.2 The Gherkin Language: Structuring Behaviour
To facilitate this process, BDD frameworks like Cucumber use a specific Domain-
-Specific Language (DSL) called Gherkin.5 Gherkin provides a simple, structured
-grammar for writing executable specifications in plain text files with a
-`.feature` extension.6 Its syntax is designed to be intuitive and accessible,
-enabling clear communication across different project roles.3
+Specific Language (DSL) called Gherkin.[^5] Gherkin provides a simple,
+structured grammar for writing executable specifications in plain text files
+with a `.feature` extension.[^6] Its syntax is designed to be intuitive and
+accessible, enabling clear communication across different project roles.[^3]
A Gherkin document is line-oriented, with most lines beginning with a specific
-keyword. The primary keywords give structure and meaning to the specifications.7
-
-| Keyword | Purpose | Simple Example |
-| ---------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
-| Feature | Provides a high-level description of a software feature and groups related scenarios.3 | Feature: User Authentication |
-| Scenario | Describes a single, concrete example of the feature's behaviour.3 | Scenario: Successful login with valid credentials |
-| Given | Sets the initial context or preconditions for a scenario.5 | Given the user is on the login page |
-| When | Describes the key action or event that triggers the behaviour being tested.1 | When the user enters their username and password |
-| Then | Specifies the expected outcome or result of the action.9 | Then the user should be redirected to the dashboard |
-| And, But | Used to add more steps to a Given, When, or Then clause without repetition, improving readability.3 | And the user's name should be displayed |
-| Background | Defines a set of steps that run before every Scenario in a Feature, used for common setup.6 | Background: Given a registered user "Alice" exists |
-| Scenario Outline | A template for running the same Scenario multiple times with different data sets.3 | Scenario Outline: Login with various credentials |
-| Examples | A data table that provides the values for a Scenario Outline.3 | username | password | outcome |
+keyword. The primary keywords give structure and meaning to the
+specifications.[^7]
+
+| Keyword | Purpose | Simple Example |
+| ---------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
+| Feature | Provides a high-level description of a software feature and groups related scenarios.[^3] | Feature: User Authentication |
+| Scenario | Describes a single, concrete example of the feature's behaviour.[^3] | Scenario: Successful login with valid credentials |
+| Given | Sets the initial context or preconditions for a scenario.[^5] | Given the user is on the login page |
+| When | Describes the key action or event that triggers the behaviour being tested.[^1] | When the user enters their username and password |
+| Then | Specifies the expected outcome or result of the action.[^9] | Then the user should be redirected to the dashboard |
+| And, But | Used to add more steps to a Given, When, or Then clause without repetition, improving readability.[^3] | And the user's name should be displayed |
+| Background | Defines a set of steps that run before every Scenario in a Feature, used for common setup.[^6] | Background: Given a registered user "Alice" exists |
+| Scenario Outline | A template for running the same Scenario multiple times with different data sets.[^3] | Scenario Outline: Login with various credentials |
+| Examples | A data table that provides the values for a Scenario Outline.[^3] | username | password | outcome |
### 1.3 The Given-When-Then Idiom: A Universal Test Pattern
For developers, the `Given-When-Then` structure is not an entirely new concept.
It is a highly effective reformulation of well-established testing patterns
-that many are already familiar with from unit testing.5 The most common parallel
-is the **Arrange-Act-Assert (AAA)** pattern, conceptualized by Bill Wake.
+that many are already familiar with from unit testing.[^5] The most common
+parallel is the **Arrange-Act-Assert (AAA)** pattern, conceptualized by Bill
+Wake.
- **Given** corresponds to **Arrange**: This phase sets up the world. It
establishes all preconditions, initializes objects, and brings the system
- under test (SUT) to the specific state required for the test. In Gherkin, this
- is where the team describes the context before the behaviour begins.5
+ under test (SUT) to the specific state required for the test. In Gherkin,
+ this is where the team describes the context before the behaviour begins.[^5]
- **When** corresponds to **Act**: This is the single, pivotal action performed
on the SUT. It's the event or trigger whose consequences are being specified.
- This phase should ideally contain only one primary action.5
+ This phase should ideally contain only one primary action.[^5]
- **Then** corresponds to **Assert**: This phase verifies the outcome. After
- the action in the `When` step, the `Then` steps check that the SUT's state has
- changed as expected. These steps should contain the assertions and should be
- free of side effects.5
+ the action in the `When` step, the `Then` steps check that the SUT's state
+ has changed as expected. These steps should contain the assertions and should
+ be free of side effects.[^5]
This connection demystifies BDD. It is not an alien methodology but a
-structured, collaborative application of a pattern developers already use.
-The power of Gherkin lies in making the Arrange-Act-Assert pattern legible and
+structured, collaborative application of a pattern developers already use. The
+power of Gherkin lies in making the Arrange-Act-Assert pattern legible and
verifiable by non-programmers, thereby turning a simple test into a piece of
shared, executable documentation.
@@ -94,21 +96,21 @@ section walks through creating a minimal, runnable test suite from scratch.
To begin, the necessary dependencies must be added and a custom test runner
configured. The `cucumber` crate is async-native and requires an async runtime
to execute tests; `tokio` is the most common choice and is used throughout the
-official documentation.12
+official documentation.[^12]
The key configuration step is defining a `[[test]]` target in `Cargo.toml`.
This tells Cargo to build a specific test executable. Setting `harness = false`
is crucial; it disables Rust's default test harness, allowing the `cucumber`
-runner to take control of the process and print its own formatted output to
-the console.13
+runner to take control of the process and print its own formatted output to the
+console.[^13]
-| Section | Key | Value / Description |
-| ------------------ | -------- | -------------------------------------------------------------------------------------------------- |
-| [dependencies] | tokio | The async runtime. Required with features like macros and rt-multi-thread.13 |
-| [dev-dependencies] | cucumber | The main testing framework crate.16 |
-| [dev-dependencies] | futures | Often needed for async operations, particularly with older examples or for specific combinators.18 |
-| [[test]] | name | The name of the test-runner file (e.g., "cucumber"). This must match the filename in tests/. |
-| [[test]] | harness | Must be set to `false` so cucumber can manage test execution and output.14 |
+| Section | Key | Value / Description |
+| ------------------ | -------- | ----------------------------------------------------------------------------------------------------- |
+| [dependencies] | tokio | The async runtime. Required with features like macros and rt-multi-thread.[^13] |
+| [dev-dependencies] | cucumber | The main testing framework crate.[^16] |
+| [dev-dependencies] | futures | Often needed for async operations, particularly with older examples or for specific combinators.[^18] |
+| [[test]] | name | The name of the test-runner file (e.g., "cucumber"). This must match the filename in tests/. |
+| [[test]] | harness | Must be set to `false` so cucumber can manage test execution and output.[^14] |
Here is a complete `Cargo.toml` configuration snippet:
@@ -133,8 +135,8 @@ harness = false
### 2.2 Directory Structure and File Organization
A well-organized project structure is vital for maintainable BDD tests. The
-standard convention separates the human-readable feature specifications from the
-Rust implementation code.18
+standard convention separates the human-readable feature specifications from
+the Rust implementation code.[^18]
```plaintext
.
@@ -155,24 +157,24 @@ The `.feature` files in `tests/features/` define *what* the system should do.
These can be read, written, and reviewed by non-technical stakeholders. The
Rust files in `tests/steps/` define *how* those behaviours are tested. This
clear boundary is a cornerstone of effective BDD practice and is strongly
-recommended.14
+recommended.[^14]
### 2.3 The `World` Object: Managing Scenario State
The `World` is the most critical concept in `cucumber-rs`. It is a user-defined
-struct that encapsulates all the shared state for a single test scenario.16
+struct that encapsulates all the shared state for a single test scenario.[^16]
Each time a scenario begins, a new instance of the `World` is created. This
instance is then passed mutably to each step (`Given`, `When`, `Then`) within
-that scenario.18
+that scenario.[^18]
This design provides a powerful mechanism for test isolation. Because each
scenario gets its private `World` instance, there is no risk of state leaking
-from one test to another, even when tests are run concurrently.20 This is a
+from one test to another, even when tests are run concurrently.[^20] This is a
significant advantage of the Rust implementation, leveraging the language's
-ownership model to solve a common and difficult problem in test automation.21
+ownership model to solve a common and difficult problem in test automation.[^21]
To create a `World`, define a struct and derive `cucumber::World`. It is also
-conventional to derive `Debug` and `Default`.12
+conventional to derive `Debug` and `Default`.[^12]
**Worked Example:** For a simple calculator application, the `World` might look
like this:
@@ -192,7 +194,7 @@ pub struct CalculatorWorld {
By default, `cucumber` will instantiate the `World` using `Default::default()`.
If a `World` requires more complex initialization (for example, starting a mock
server or connecting to a test database), provide a custom constructor function
-using the `#[world(init = ...)]` attribute.20
+using the `#[world(init = ...)]` attribute.[^20]
### 2.4 Your First `main` Test Runner
@@ -201,10 +203,10 @@ function in the test target file (for example, `tests/cucumber.rs`). This
function acts as the entry point for the test suite.
Because `cucumber-rs` is async, the `main` function must be an `async fn` and
-is typically annotated with `#[tokio::main]`.13 The core of the function is a
-single line that invokes the test runner:
+is typically annotated with `#[tokio::main]`.[^13] The core of the function is
+a single line that invokes the test runner:
-`YourWorld::run("path/to/features").await`.16
+`YourWorld::run("path/to/features").await`.[^16]
**Worked Example:**
@@ -249,8 +251,8 @@ async fn main() {
}
```
-At this point, there is a complete, albeit empty, test suite. Running `cargo
-test --test cucumber` will compile the runner, which will then discover
+At this point, there is a complete, albeit empty, test suite. Running
+`cargo test --test cucumber` will compile the runner, which will then discover
`.feature` files in `tests/features`, find no matching steps, and report them
as undefined.
@@ -262,21 +264,21 @@ procedural macros to make this connection seamless and type-safe.
### 3.1 The `#[given]`, `#[when]`, and `#[then]` Macros
-The core of step definition is a set of attribute macros: `#[given]`, `#[when]`,
-and `#[then]`.12 You apply these macros to Rust functions. When the test
-runner encounters a Gherkin step, it looks for a function annotated with the
-corresponding macro and a matching text pattern.
+The core of step definition is a set of attribute macros: `#[given]`,
+`#[when]`, and `#[then]`.[^12] You apply these macros to Rust functions. When
+the test runner encounters a Gherkin step, it looks for a function annotated
+with the corresponding macro and a matching text pattern.
Each step definition function must accept a mutable reference to the `World`
-struct as its first argument (for example, `world: &mut CalculatorWorld`).18
-This affords the function the ability to modify the shared state for the current
-scenario.
+struct as its first argument (for example, `world: &mut CalculatorWorld`).[^18]
+This affords the function the ability to modify the shared state for the
+current scenario.
A key design choice in `cucumber-rs` is the strict separation of these step
types. A function marked with `#[then]` cannot be used to satisfy a `Given`
-step in a feature file.20 This is a deliberate feature, not a limitation.
-It encourages developers to maintain the clean Arrange-Act-Assert structure
-by preventing them from accidentally using assertion logic during setup, or
+step in a feature file.[^20] This is a deliberate feature, not a limitation. It
+encourages developers to maintain the clean Arrange-Act-Assert structure by
+preventing them from accidentally using assertion logic during setup, or
performing actions during verification. This discipline leads to more readable,
robust, and maintainable tests.
@@ -312,23 +314,23 @@ fn check_result(world: &mut CalculatorWorld, expected: i32) {
### 3.2 Capturing Arguments: Regex vs. Cucumber Expressions
-To make steps dynamic, captured fragments of the Gherkin text must be passed
-as arguments to the corresponding Rust functions. `cucumber-rs` supports two
-mechanisms for this: regular expressions and Cucumber Expressions.16
+To make steps dynamic, captured fragments of the Gherkin text must be passed as
+arguments to the corresponding Rust functions. `cucumber-rs` supports two
+mechanisms for this: regular expressions and Cucumber Expressions.[^16]
- **Cucumber Expressions (**`expr = "..."`**)**: This is the recommended
default. They are less powerful than regex but are more readable and
explicitly designed for this purpose. They provide built-in parsing for
- common types like `{int}`, `{float}`, `{word}`, and `{string}` (in quotes).16
- The framework automatically handles parsing the captured string into the
- corresponding Rust type in your function signature.
+ common types like `{int}`, `{float}`, `{word}`, and `{string}` (in
+ quotes).[^16] The framework automatically handles parsing the captured string
+ into the corresponding Rust type in your function signature.
- **Regular Expressions (**`regex = "..."`**)**: For more complex matching
needs, full regex syntax can be used. Capture groups `(...)` in the regex
- correspond to function arguments.18 The framework will still attempt to parse
- the captured `&str` into the function's argument type. It is a best practice
- to anchor the regex with `^` and `$` to ensure the entire step text is
- matched, preventing partial or ambiguous matches.18.
+ correspond to function arguments.[^18] The framework will still attempt to
+ parse the captured `&str` into the function's argument type. It is a best
+ practice to anchor the regex with `^` and `$` to ensure the entire step text
+ is matched, preventing partial or ambiguous matches.[^18].
| Feature | Cucumber Expression Example | Regex Example | Recommendation |
| --------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
@@ -341,27 +343,27 @@ mechanisms for this: regular expressions and Cucumber Expressions.16
The `Then` steps are where you verify the system's state. The most
straightforward way to do this is with Rust's standard assertion macros, like
-`assert_eq!` or `assert!`.16 If an assertion fails, the thread will panic, and
-`cucumber` will mark the step as failed.
+`assert_eq!` or `assert!`.[^16] If an assertion fails, the thread will panic,
+and `cucumber` will mark the step as failed.
However, a more idiomatic and powerful approach is to have your step functions
-return a `Result`.20 A step that returns
+return a `Result`.[^20] A step that returns
`Ok(())` passes, while one that returns an `Err(...)` fails. This has two major
benefits:
1. **Cleaner Code:** Using the `?` operator propagates errors from the
- application logic or from parsing steps, leading to more concise and readable
- code.
+ application logic or from parsing steps, leading to more concise and
+ readable code.
2. **Richer Failure Messages:** A panic from an `assert!` often gives a limited
error message. Returning a custom error type that implements
- `std::error::Error` provides detailed, contextual information about *why* the
- test failed. This is invaluable for debugging.
+ `std::error::Error` provides detailed, contextual information about *why*
+ the test failed. This is invaluable for debugging.
Rust's error handling philosophy is built around the `Result` enum for
recoverable errors, and a test failure is a recoverable error from the test
-runner's perspective.22 Embracing this pattern in your step definitions is a
+runner's perspective.[^22] Embracing this pattern in your step definitions is a
significant best practice.
**Worked Example (using** `Result`**):**
@@ -408,13 +410,14 @@ asynchronous operations.
### 4.1 Data-Driven Testing: `Scenario Outline` and `Examples`
Often, the same behaviour must be tested with various inputs and expected
-outputs. Writing a separate `Scenario` for each case would be highly repetitive.
-Gherkin solves this with the `Scenario Outline` keyword.3
+outputs. Writing a separate `Scenario` for each case would be highly
+repetitive. Gherkin solves this with the `Scenario Outline` keyword.[^3]
A `Scenario Outline` acts as a template. You write the steps using placeholders
-enclosed in angle brackets, like `` or `