Framework
cucumber-rs/cucumber avatar
cucumber-rs/cucumber

cucumber-rs, Cucumber for Rust without an external test runner

Cucumber testing framework for Rust. Fully native, no external test runners or dependencies.

745 stars90 forksRustApache-2.0

At a glance

What is it?
cucumber-rs/cucumber implements the Cucumber testing framework natively in Rust, with no external runner, no unsafe code and a parser for the full Gherkin language. Scenarios live in .feature files, steps are Rust functions matched by Cucumber Expression or regular expression, and the output formats cover Cucumber JSON, JUnit XML, libtest JSON and tracing.
Who is it for?
Adopt cucumber-rs if you want Gherkin feature files in a Rust codebase and want the whole thing to stay inside cargo test with no external runner or JVM involved. Skip it if your team is more comfortable with Rust's own test harness and plain unit tests, since the added value of feature files is mostly about shared language with non-engineers.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 89 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the project is and the shape of a test

cucumber-rs is an implementation of the Cucumber testing framework for Rust, described as fully native with no external test runners or dependencies. The badge for unsafe forbidden and the security tooling it links to make it explicit that the crate does not use unsafe code at all.

The shape of a test is the familiar Gherkin one. A feature file describes a scenario:

gherkin
Feature: Eating too much cucumbers may not be good for you

  Scenario: Eating a few isn't a problem
    Given Alice is hungry
    When she eats 3 cucumbers
    Then she is full

and the steps are implemented as Rust functions. The interesting part is how a step is matched to a function, and there are two mechanisms. The first is a Cucumber Expression, which is the parameterised pattern the Cucumber project standardised on:

rust
#[given(expr = "{word} is hungry")] // Cucumber Expression
async fn someone_is_hungry(w: &mut World, user: String) {

and the second is a regular expression, for the cases where the step text varies in a way an expression cannot express:

rust
#[when(regex = r"^(?:he|she|they) eats? (\d+) cucumbers?$")]
async fn eat_cucumbers(w: &mut World, count: usize) {

So the Gherkin step text becomes a function argument. The word in the expression is bound to the user parameter, and the digits in the regular expression are bound to the count. That binding is the reason to use Cucumber in the first place, because the scenario text and the function signature cannot drift apart silently.

The World trait is where the state lives between steps, and the README's example derives it with the cucumber::World derive macro, so the struct's fields are the state a scenario carries.

The runner is a single call in main:

rust
#[tokio::main]
async fn main() {
    World::run("tests/features/readme").await;
}

and the Cargo.toml needs a test target with the harness disabled:

toml
[[test]]
name = "readme"
harness = false  # allows Cucumber to print output instead of libtest

That harness setting is the integration point with cargo. With the default libtest harness, cargo owns the output and the framework is a guest; with harness false, the framework owns the output and cargo just runs the binary. Every downstream decision about reporting follows from that choice.

How step matching and the World work

The step-matching mechanism is where most of the practical value sits, and it is worth understanding the two forms.

A Cucumber Expression is a parameterised pattern. The word in curly braces is a placeholder, and the framework matches the step text against the pattern, extracts the values and hands them to the function as arguments. The advantage over a regular expression is that an expression is legible to a non-programmer, which matters because the step text is written in the feature file by whoever wrote the specification. A step that reads Given Alice is hungry is a sentence, and the expression for it is a sentence with a slot in it.

A regular expression is the escape hatch, and it exists for the cases an expression cannot express. The README's example is the he, she, they alternation with an optional s on the verb, which is exactly the sort of variation that a strict expression cannot cover without becoming unreadable. The trade is that a regular expression in a step definition is a piece of regular expression in a file that non-programmers may read, and regular expressions in a business-readable document are a maintenance liability.

The world struct is the other half of the design. The README's example derives Default on it, so a scenario starts from a known state, and the fields are what the steps read and write. Deriving the world macro rather than implementing the trait by hand means the framework can generate the machinery that ties the struct to the runner.

The consequence is that a scenario reads almost exactly like the specification, and the assertions live in Rust rather than in the feature file. That is the right division. The feature file says what should happen, the step implementation says how it is checked, and a change in the check does not require touching the specification.

The output formats and the feature flags

The Cargo features are what determine how results reach the outside world, and the list explains itself.

The macros feature is on by default and enables the step attributes and the auto-wiring, which is what makes the given, when and then attributes work.

The timestamps feature enables timestamp collection for all Cucumber events, and both output features imply it, because a report without timings is much less useful.

output-json enables the Cucumber JSON format, and output-junit enables JUnit XML, which is the format most CI systems and issue trackers already know how to render. The libtest feature enables compatibility with the JSON output format of Rust's own libtest harness, and the README says this is useful for IntelliJ Rust plugin integration, which is the pragmatic reason to turn it on rather than the reason it exists.

The tracing feature integrates with the tracing crate, so the events a scenario produces can be emitted into whatever tracing subscriber the application already has. That is the feature that makes the framework usable inside a service that has tracing set up for other reasons, rather than one more separate reporting path.

The dependency story is short and worth noting. The full gamut of the Gherkin language is implemented by a separate gherkin crate, and most features of the language are parsed already and reachable through the relevant structs. So the parser is a dependency rather than part of this repository, and the README's claim of no external dependencies means no external test runner, not no crates.

There is one known issue listed. Scenario Outline is treated the same as Outline or Example in the parser, tracked as gherkin issue 19. That is a real limitation for anyone using scenario outlines with examples tables, and it is the kind of thing you would rather find in the README than in your CI.

The licence is Apache-2.0 or MIT at your option, the crate needs Rust 1.88 or newer, and the releases are steady: 0.22.0 in December 2025, 0.22.1 later that month and 0.23.0 in April 2026, with the last push on 2026-07-27.

How this compares with the alternatives

The comparison to make is with the original Cucumber, and the difference is in everything except the language.

Cucumber itself is a multi-language framework, and this crate keeps the specifications portable while replacing the runtime. The Gherkin feature file, the step wording, the Cucumber Expressions and the output formats are all the same, so a team porting an existing suite to Rust keeps the specifications and rewrites the step definitions. That is the strongest argument for this project, because the expensive part of a Cucumber suite is the feature files, and those are portable.

What you give up is the surrounding tooling. Whatever integrations, reporters and community libraries exist for Cucumber elsewhere, this crate itself offers only the output formats listed in its Cargo features. If your organisation already has hundreds of feature files and a custom reporting pipeline, porting the specifications is worthwhile but the tooling around them does not come with it.

The other comparison is with Rust's own testing conventions. Rust has a strong norm of testing at the unit level with the built-in harness, and the built-in harness is fast, parallel and has excellent failure output. Most Rust projects never need Cucumber, because the problems Cucumber solves, where a specification is shared with people who do not read the code, do not arise for a library.

The case where it does arise is a product with a non-engineering stakeholder. When the acceptance criteria live in a document that a product owner reads and edits, and the tests derive from that document, the value is in the shared artefact rather than in the testing technology. The cargo integration keeps this cheap because the feature files live next to the code and the whole thing runs with cargo test.

The honest caveat about the harness false setting is that you give up cargo's own test reporting, including the parallel execution and the per-test timing, and you take on the responsibility of the runner's own output. In exchange you get Gherkin output, which is the point. If your CI is set up to parse libtest JSON, the libtest feature exists to bridge that, but the framework's native output is Cucumber's.

A third option worth naming is not writing Cucumber at all and instead keeping the specification as a document that a test generator reads. That is a different design, and for some teams it is a better one, because the specification becomes typed rather than text.

Editorial conclusion

Adopt cucumber-rs if you want Gherkin feature files in a Rust codebase and want the whole thing to stay inside cargo test with no external runner or JVM involved. Skip it if your team is more comfortable with Rust's own test harness and plain unit tests, since the added value of feature files is mostly about shared language with non-engineers. Verify first by adding a single feature file with one scenario, and confirm your CI collects the output format you need, since the default output is Cucumber's own rather than libtest's.

Frequently asked questions

What is cucumber-rs?

It is an implementation of the Cucumber testing framework for Rust that is fully native, with no external test runner and no unsafe code. Scenarios are written in .feature files and the steps are implemented as Rust functions matched by Cucumber Expression or regular expression.

How do I match a Gherkin step to a Rust function?

With the given, when and then attributes, using either a Cucumber Expression such as expr = "{word} is hungry" or a regular expression such as regex = r"^(?:he|she|they) eats? (\d+) cucumbers?$". The matched values are bound to the function's parameters.

What do the Cargo features of cucumber-rs control?

macros is on by default and enables the step attributes and auto-wiring, timestamps enables event timestamps, output-json and output-junit enable the Cucumber JSON and JUnit XML report formats and both imply timestamps, libtest enables compatibility with Rust's libtest JSON output for IntelliJ integration, and tracing integrates with the tracing crate.

How does cucumber-rs integrate with cargo test?

By adding a test target in Cargo.toml with harness = false, so that Cucumber prints the output instead of libtest. The README notes this is what allows Cucumber to print its own output rather than the default test harness output.

What are the known limitations of cucumber-rs?

Scenario Outline is treated the same as Outline or Example in the parser, which is tracked as issue 19 in the gherkin crate. The Gherkin language itself is implemented in a separate crate, and the project requires Rust 1.88 or newer.

Official sources

  1. cucumber-rs/cucumber on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cucumber-rs-cucumber.svg)](https://hysenlabs.com/projects/cucumber-rs-cucumber)

Was this useful?