cucumber-rs

repository·main·Indexed 20 days ago

https://github.com/cucumber-rs/cucumber

A fully native implementation of the Cucumber testing framework for Rust. It enables behavior-driven development (BDD) using Gherkin syntax without external test runners. The framework features a modular architecture with replaceable Parser, Runner, and Writer components, and supports asynchronous step definitions via the World trait. Version 0.23.0 includes optional features for JSON/JUnit output, tracing, and libtest compatibility.

Tokens
35.3K
Snippets
110
Records
135
Agent score
71%

What's inside cucumber

  1. Configure test suite output formats

    main

    Cucumber allows you to output test results in several formats to suit different needs, such as human-readable terminal output, machine-readable reports for CI/CD, or integration with specific IDEs.

    Available output methods include:

    • Terminal: Standard human-readable output in the console.
    • JUnit XML report: XML format compatible with JUnit-based test runners and CI tools.
    • Cucumber JSON format: Standard Cucumber JSON format for compatibility with other Cucumber ecosystems.
    • Multiple outputs: Configuring the runner to emit several formats simultaneously.
    • tracing integration: Using the Rust tracing ecosystem for structured logging of test execution.
    • IntelliJ Rust (libtest) integration: Formatting output to be compatible with IntelliJ Rust's test runner.
  2. Define a World to manage test state

    main

    In cucumber, the World is a struct that holds the state for your test scenarios. You implement the cucumber::World trait on your struct using the #[derive(Debug, Default, cucumber::World)] macro. This state is then passed as a mutable reference (&mut World) to your step definitions, allowing you to share data between Given, When, and Then steps.

    #[derive(Debug, Default, cucumber::World)]
    struct World {
        user: Option<String>,
        capacity: usize,
    }
  3. Implement a custom `Runner`

    main

    A Runner is a core abstraction in Cucumber that transforms a Stream of gherkin::Features into a Stream of event::Cucumber events. You can implement the Runner trait to customize how scenarios are executed, how tags are handled, or how the execution lifecycle is managed.

    To implement a custom Runner, you must define:

    1. type Cli: The type of command-line options your runner accepts (e.g., cli::Empty if no options are needed).
    2. type EventStream: The type of the stream of events emitted by the runner.
    3. The run method: This method takes a stream of features and returns the EventStream.

    When implementing run, you typically chain streams of events (like Started, Finished, and the results of feature/scenario execution) to provide a complete lifecycle of cucumber events.

    impl cucumber::Runner<AnimalWorld> for CustomRunner {
        type Cli = cli::Empty;
        type EventStream = LocalBoxStream<'static, parser::Result<Event<event::Cucumber<AnimalWorld>>>>;
    
        fn run<S>(self, features: S, _: Self::Cli) -> Self::EventStream
        where
            S: Stream<Item = parser::Result<gherkin::Feature>> + 'static,
        {
            stream::once(future::ok(event::Cucumber::Started))
                .chain(
                    features
                        .map_ok(|f| Self::execute_feature(f).map(Ok))
                        .try_flatten(),
                )
                .chain(stream::once(future::ok(event::Cucumber::Finished)))
                .map_ok(Event::new)
                .boxed_local()
        }
    }
  4. How Cucumber's architecture works

    main

    Cucumber is built as a modular framework composed of three primary, replaceable components. This design allows you to extend the framework for exotic requirements (such as sourcing features from a distributed queue or executing them on a remote cluster) by implementing custom versions of these components.

    The three core components are:

    1. Parser: Responsible for sourcing feature files. The default implementation is parser::Basic, which parses standard .feature files.
    2. Runner: Responsible for executing the scenarios provided by a Parser and emitting events. The default implementation is runner::Basic, which executes scenarios concurrently.
    3. Writer: Responsible for outputting the events emitted by the Runner. The default implementation is writer::Basic, which outputs to STDOUT.
  5. Use the Rule keyword in Gherkin

    main

    The Rule keyword is used in Gherkin feature files to represent a specific business rule that needs to be implemented. It serves as a grouping mechanism for one or more Scenarios that illustrate that particular rule.

    Note that using Rule in your Gherkin files requires no additional implementation work in your Rust code; it is purely for organizing and providing additional context to your features.

    Feature: Animal feature
        
      Rule: Hungry cat becomes satiated
          
        Scenario: If we feed a hungry cat it will no longer be hungry
          Given a hungry cat
          When I feed the cat
          Then the cat is not hungry
        
      Rule: Satiated cat remains the same
          
        Scenario: If we feed a satiated cat it will not become hungry
          Given a satiated cat
          When I feed the cat
          Then the cat is not hungry
  6. Understand tag inheritance in Gherkin

    main

    Tags applied to higher-level elements are automatically inherited by their children:

    • Feature and Rule tags are inherited by Scenario, Scenario Outline, and Examples.
    • Scenario Outline tags are inherited by its Examples blocks.

    Note that you can also apply specific tags directly to individual Examples blocks within a Scenario Outline to differentiate them.

    Important: You cannot place tags above Background or individual steps (Given, When, Then, etc.).

    @feature
    Feature: Animal feature
    
      @scenario
      Scenario Outline: If we feed a hungry animal it will no longer be hungry
        Given a hungry <animal>
        When I feed the <animal> <n> times
        Then the <animal> is not hungry
    
      @home
      Examples:
        | animal | n |
        | cat    | 2 |
        | dog    | 3 |
    
      @dire
      Examples:
        | animal | n |
        | lion   | 1 |
        | wolf   | 1 |
  7. Use Scenario Outline to run scenarios with multiple data sets

    main

    The Scenario Outline keyword allows you to run the same scenario multiple times using different combinations of values provided in an Examples table.

    Unlike [data tables], which process a table within a single step, a Scenario Outline executes the entire scenario separately for every row in the Examples table.

    Key behaviors:

    • Template Replacement: Placeholders in the format <template> are replaced by the values from the Examples table cells. This replacement occurs during the parsing stage and applies even inside [doc strings] and [data tables].
    • Step Matching: Because templates are replaced before matching, your step definition functions (using regex or expr) will receive the actual values from the table rows as arguments.
    • API Access: When using filter_run() or other APIs that access the gherkin::Scenario::examples::table::rows field, only the row currently being executed is accessible.
    Feature: Animal feature
    
      Scenario Outline: If we feed a hungry animal it will no longer be hungry
        Given a hungry <animal>
        When I feed the <animal> <n> times
        Then the <animal> is not hungry
    
      Examples: 
        | animal | n |
        | cat    | 2 |
        | dog    | 3 |
        | 🦀     | 4 |
  8. How the World abstraction works

    main

    The World is a shared, mutable state object that holds the data for your test scenario. Cucumber constructs a new instance of your World using Default::default() for every individual scenario, ensuring test isolation.

    To define a World, derive the World trait on a struct. Step functions then receive a mutable reference to this struct (&mut MyWorld) to read or modify the state.

    #[derive(Debug, Default, World)]
    pub struct AnimalWorld {
        cat: Cat,
    }
    
    #[given("a hungry cat")]
    fn hungry_cat(world: &mut AnimalWorld) {
        world.cat.hungry = true;
    }
  9. How Writers and event::Cucumber work together

    main

    The Writer sits at the end of the Cucumber execution pipeline. The Runner produces a stream of event::Cucumber<W> events, where W is your World type.

    Events follow a hierarchical structure:

    1. event::Cucumber: The top-level wrapper for all events.
    2. event::Feature: Represents the lifecycle of a Gherkin feature (e.g., Started, Finished).
    3. event::Scenario: Nested within a feature, representing scenario lifecycle.
    4. event::Step: Nested within a scenario, representing individual step execution (e.g., Started, Passed, Failed, Skipped).

    By implementing Writer, you can intercept these specific moments to generate custom reports or real-time console feedback.

  10. Implement a custom `Parser`

    main

    A Parser is an abstraction that represents anything capable of emitting a Stream of Gherkin Features. You can implement the Parser trait to programmatically generate features for execution instead of reading them from files.

    To implement a custom Parser, you must define:

    1. type Cli: The type of CLI options your parser accepts (use cli::Empty if no options are needed).
    2. type Output: The type of the stream emitted, which must be a Stream of parser::Result<gherkin::Feature>.
    3. The parse method: This method takes the input (of type I) and CLI options, returning the feature stream.

    This is useful for scenarios where features are generated dynamically or stored in a non-standard format.

    struct CustomParser;
    
    impl<I> cucumber::Parser<I> for CustomParser {
        type Cli = cli::Empty; // we provide no CLI options
        type Output = stream::Once<future::Ready<parser::Result<gherkin::Feature>>>;
    
        fn parse(self, _: I, _: Self::Cli) -> Self::Output {
            // Return a stream containing your manually constructed gherkin::Feature
            stream::once(future::ok(gherkin::Feature {
                keyword: "Feature".into(),
                name: "My Dynamic Feature".into(),
                // ... rest of the feature definition
                ..Default::default()
            }))
        }
    }
  11. Run scenarios in isolation with @serial

    main

    The cucumber crate provides built-in support for the @serial tag. Any scenario marked with @serial will be executed in isolation, ensuring no other scenarios are running concurrently during its execution. This is useful for tests that require exclusive access to resources.

    Tip: If you want to run your entire test suite serially instead of tagging every feature, use the --concurrency=1 CLI option.

    @serial
    Scenario: If we feed a satiated cat it will not become hungry
      Given a satiated cat
      When I feed the cat
      Then the cat is not hungry