View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
/Tools Library/Cucumber
Tools Library · We run this

Cucumber review: BDD tutorial, pros and cons

Cucumber runs specifications written in Gherkin, a structured plain-language format of Given, When and Then steps, by matching each step to a step definition in code. Official implementations cover Java and other JVM languages, JavaScript and TypeScript, Ruby and C++. It is free under the MIT licence and, since December 2024, owned by its open source community rather than a company.

QAble runs this tool on client engagements. The assessment below is from that experience.

last reviewed
13 October 2026
version covered
Cucumber-JVM 8 (also cucumber-js 13)
licence
Open source

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

Cucumber at a glance

Official site
Vendor
Open Source Collective (community owned)
Category
BDD frameworks
Licence
Open source (MIT)
Platforms
JVM, Android, Node.js, Ruby, C++
Languages
Gherkin, Java, Kotlin, Scala, JavaScript, TypeScript, Ruby, C++
Integrates with
JUnit, TestNG, Selenium, Playwright, Serenity
First released
2008
Version reviewed
Cucumber-JVM 8 (also cucumber-js 13)

Verdict

Our verdict on Cucumber

Strengths

  • Scenarios in plain language that product owners can read and challenge, so acceptance criteria and tests stay the same artefact
  • Cucumber Expressions such as {int} and {string} turn step text into typed arguments without hand-written regular expressions
  • On the JVM it runs as a JUnit Platform engine, so Maven, Gradle and IDEs discover scenarios natively
  • Tag expressions select suites at run time, for example @smoke and not @wip, without separate runner classes
  • Free under MIT, community owned since December 2024, with no paid tier holding features back

Weaknesses

  • Every scenario needs glue code, so automating tests nobody outside QA reads costs double for no benefit
  • Step libraries drift into ambiguity and hidden shared state unless someone actively curates them
  • Cucumber-JVM 8 requires Java 17 and deprecates the JUnit 4 and TestNG runners many tutorials still use
  • The built-in HTML report is a single page; living documentation and trends need Serenity BDD or a test management tool

Best for

Teams that genuinely practise behaviour-driven development: product owners, developers and testers agree examples in a discovery session, and the scenarios are written before the code and read afterwards by people who do not program. In that setting the Gherkin layer is the record of a conversation, and keeping it executable is worth its cost. It also suits regulated work where acceptance criteria must be traceable to evidence, because a scenario is both the requirement and the test.

Not for

Teams that write Gherkin after the feature is built, to automate tests nobody outside QA will read. That describes most Cucumber suites we inherit. The plain-language layer then has no audience, and every test costs a feature file, a step definition and the glue between them. Unit and component tests, API contract checks and technical end-to-end flows are clearer written directly in JUnit, Playwright or pytest.

Setup cost

Low mechanically, high culturally. The Maven archetype or one npm install gives a running scenario inside the hour, and on the JVM the JUnit Platform engine means Maven, Gradle and the IDE all pick scenarios up without a custom runner. The real setup is agreeing a step vocabulary, a domain language for scenarios and who writes them. Without that, the first fifty scenarios set patterns, mostly bad ones, that the next five hundred copy.

Maintenance burden

Higher than plain test code, because each change passes through two layers. The usual decay is step definitions written to be reused at any cost, until one sentence means different things in different features and a hidden shared object carries state between steps. Budget regular step library reviews, and treat the major upgrades seriously: Cucumber-JVM 8 moved the baseline to Java 17, made Jackson an opt-in dependency for several report plugins and deprecated the JUnit 4 and TestNG runners.

Honest limitations

It does not make a team do BDD, which is the most common misunderstanding in this category. It is a runner, not a framework: browser control, API calls, waiting and test data all come from libraries you add. Debugging crosses an extra layer of indirection. Reports are plain without extra tooling such as Serenity BDD or a test management tool. And it only rewards readable scenarios; badly written Gherkin is harder to maintain than the code it hides.

How it works

What is Cucumber and how does it work?

From feature file to pickles

Cucumber reads .feature files with the Gherkin parser, which turns each scenario into a pickle: a flat list of steps with Background steps prepended and Scenario Outline rows expanded into one pickle per example. Tags are inherited from the feature and rule. This is why a single outline with twenty examples appears as twenty tests in a report, and why filtering by tag or by line number works on individual examples. The parser is a shared component used by every official implementation, so the Gherkin syntax, including Rule and the localised keywords in dozens of spoken languages, behaves the same in Java, JavaScript and Ruby.

Step definitions are pattern matches

Each pickle step is matched against every registered step definition, written either as a Cucumber Expression such as I have {int} items in my basket or as a regular expression. Exactly one definition must match. None gives an undefined step and a generated snippet to paste in; two gives an ambiguous step error. Parameter types convert the captured text into typed arguments, and you can register your own, for example a {customer} type that looks up a test customer by name. Because matching is global across the glue path, step text is effectively a shared vocabulary for the whole suite, which is both the strength and the maintenance burden.

A new world for every scenario

Cucumber creates fresh instances of the step definition classes, or a fresh World object in JavaScript, for every scenario and throws them away at the end. State therefore passes between steps of one scenario through fields, and never between scenarios, which keeps scenarios independent by default. On the JVM, sharing state across several step classes is done with dependency injection: PicoContainer for plain constructor injection, or Spring, Guice or Jakarta CDI modules when the application already uses them. Hooks such as @Before and @After run around each scenario; @BeforeAll and @AfterAll run once per run.

Running inside the JUnit Platform

On the JVM, the recommended runner is cucumber-junit-platform-engine. Cucumber registers as a test engine, so any tool that speaks the JUnit Platform discovers features and scenarios as tests. Maven Surefire does not yet discover resource-based tests directly, so a small class annotated with @Suite and @IncludeEngines("cucumber") points it at the features, while Gradle can discover them from a test definitions directory. Configuration comes from junit-platform.properties or @ConfigurationParameter annotations, and parallel execution uses the platform’s fork-join pool. The older cucumber-junit runner for JUnit 4 and cucumber-testng are deprecated for removal in v8.

Strengths

What it does well

Executable acceptance criteria

A well-written scenario states a business rule through a concrete example, and running it proves the rule still holds. Examples agreed before development become the definition of done, and a failing scenario names the broken rule in words a product owner understands, rather than a stack trace only a developer can read.

Typed parameters with Cucumber Expressions

Built-in types such as {int}, {float}, {word} and {string} capture arguments and convert them, and @ParameterType registers domain types, so a step can receive a Customer or a Money value directly. That moves parsing out of step bodies and gives clearer errors when a scenario uses a value the code cannot interpret.

Data tables and examples

Data tables pass structured input to a single step, and @DataTableType converts each row into an object. Scenario Outline with an Examples table runs the same flow across many inputs. Used well, this keeps the rule visible in the feature file and the data variation alongside it, instead of buried in a loop in the code.

Tag expressions for suite selection

Tags on features, rules, scenarios and examples combine with and, or and not, so one property selects @smoke, excludes @wip or runs @payments and not @slow. On the JUnit Platform, tags also map to JUnit tags, so Maven’s groups parameter can select them. One set of features can therefore feed pull request, nightly and release pipelines.

Standard report formats

Built-in plugins write an HTML report, JSON, JUnit XML, a timeline, rerun files and Cucumber Messages, the newline-delimited JSON stream that tools such as Serenity BDD, Xray and Zephyr can consume. A rerun file lists failed scenarios so a second pass runs only those. Optional publishing to reports.cucumber.io gives a shareable link without hosting anything.

Parallel scenarios on the JVM

Setting cucumber.execution.parallel.enabled runs scenarios concurrently on the JUnit Platform, with a dynamic strategy based on cores or a fixed thread count. Scenarios that touch a shared resource can be tagged and mapped to exclusive resource locks, so they never overlap while everything else runs in parallel. cucumber-js runs parallel workers with --parallel.

Pricing

Cucumber pricing and licensing

Cucumber is free and open source under the MIT licence in its official implementations, with no paid edition or feature tiers. In December 2024 the name, trademarks and cucumber.io domain moved from SmartBear to the Open Source Collective, and the project is funded by sponsorship. The real costs are people: the time to run discovery sessions, write good scenarios and curate the step library, plus the browser, API and reporting tools you combine it with.

  • Licence: MIT for Cucumber-JVM, cucumber-js and Cucumber-Ruby, stated in each repository.
  • No tiers: parallel execution, tag filtering, every report plugin and the Cucumber Reports publishing service are free.
  • Funding: the project accepts sponsorship through Open Collective; supporting it is optional and buys no features.
  • Hidden cost: the glue layer. Every automated check needs a step definition as well as the test logic, so a suite that does not use the plain-language layer pays roughly twice for each test.
  • Hidden cost: upgrades. Majors change baselines and plugins, for example Java 17 and opt-in Jackson in Cucumber-JVM 8, so plan time per major release.

Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.

Getting started

Cucumber tutorial: getting started

Setup, step by step

  1. Install a JDK, version 17 or later, and Maven.
  2. Generate a project from the official cucumber-archetype at version 8.0.4.
  3. Open the project: features live under src/test/resources, step definitions under src/test/java.
  4. Write a feature file describing one rule with one concrete example.
  5. Run mvn test, copy the generated snippets for undefined steps into a step class, and implement them.
  6. Run mvn test again and confirm the scenario passes and the pretty plugin prints each step.

Install

mvn archetype:generate \
  -DarchetypeGroupId=io.cucumber \
  -DarchetypeArtifactId=cucumber-archetype \
  -DarchetypeVersion=8.0.4 \
  -DgroupId=com.example -DartifactId=shop-specs \
  -Dpackage=com.example.shop -Dversion=1.0.0-SNAPSHOT \
  -DinteractiveMode=false
cd shop-specs && mvn test

A first example

// src/test/resources/com/example/shop/discount.feature
//   Feature: Loyalty discount
//     Rule: Gold members get 10% off orders over 100
//       Scenario: Gold member over the threshold
//         Given a gold member
//         When they place an order worth 120.00
//         Then they pay 108.00

// src/test/java/com/example/shop/RunCucumberTest.java
@Suite
@IncludeEngines("cucumber")
@SelectPackages("com.example.shop") // feature files in the same package path
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.shop")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
final class RunCucumberTest {}

// src/test/java/com/example/shop/DiscountSteps.java
// A fresh instance per scenario, so fields never leak between scenarios.
// Since v8, step classes and methods may be package private.
class DiscountSteps {
  private Customer customer;
  private Order order;

  @Given("a gold member")
  void aGoldMember() { customer = Customer.gold(); }

  // {bigdecimal} converts "120.00" to a BigDecimal
  @When("they place an order worth {bigdecimal}")
  void theyPlaceAnOrder(BigDecimal amount) { order = Checkout.place(customer, amount); }

  @Then("they pay {bigdecimal}")
  void theyPay(BigDecimal expected) {
    assertThat(order.total()).isEqualByComparingTo(expected);
  }
}

What trips people up first

Scenarios that do not run at all, or steps reported as undefined although the definition exists. Both come from the glue and feature paths not matching. The runner must select the package or classpath folder that holds the .feature files, and the glue property must name the package that holds the step classes; with Maven Surefire, a @Suite class is required because Surefire cannot yet discover feature files on its own. The second hurdle is following an older tutorial: @RunWith(Cucumber.class) is the deprecated JUnit 4 runner, @CucumberOptions does not configure the JUnit Platform engine, and Cucumber-JVM 8 will not run on Java 11.

In practice

Using Cucumber in practice

Organising features, steps and configuration

Organise feature files by business capability, not by page or by test type: features/checkout, features/loyalty, features/returns, with one rule per Rule block and a handful of examples each. Keep step definitions grouped by domain concept rather than by feature file, because steps are global and a file per feature encourages near-duplicates. Put cross-cutting setup, such as starting a browser or resetting data, in hook classes, and keep browser or API code out of step bodies: a step should read like one sentence that calls a page object, a client or a test data builder. Hold configuration in junit-platform.properties so the IDE, Maven and CI all run the same way, and set the naming strategy so Surefire reports show feature and scenario names rather than numbers.

# src/test/resources/junit-platform.properties
cucumber.plugin=pretty, html:target/cucumber.html, junit:target/cucumber.xml, rerun:target/rerun.txt
cucumber.glue=com.example.shop
# Skip work in progress by default; override with -Dcucumber.filter.tags
cucumber.filter.tags=not @wip
# Readable names in Surefire reports (Surefire 3.5.4 and later)
cucumber.junit-platform.naming-strategy=long
# Run scenarios in parallel, one thread per core
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=dynamic
# Scenarios tagged @db never run alongside each other
cucumber.execution.exclusive-resources.db.read-write=database

Sharing state between step classes with PicoContainer

Once steps are grouped by domain, one scenario touches several step classes, and they need to share state such as the current customer or the last API response. Do not use static fields: they leak between scenarios and break as soon as you run in parallel. Add cucumber-picocontainer and declare a plain scenario context class as a constructor parameter in each step class; PicoContainer creates one instance per scenario and hands the same instance to every class that asks for it. Note the v8 exception: with PicoContainer, step classes must still be public with a public constructor, even though v8 lets other object factories use package-private classes.

// One instance per scenario, shared by every step class that asks for it
public class ScenarioState {
  public Customer customer;
  public ApiResponse lastResponse;
}

// PicoContainer needs public classes and constructors, even in v8
public class CustomerSteps {
  private final ScenarioState state;

  public CustomerSteps(ScenarioState state) { this.state = state; }

  @Given("a gold member")
  public void aGoldMember() { state.customer = TestCustomers.gold(); }
}

public class OrderSteps {
  private final ScenarioState state;
  private final OrdersClient orders = new OrdersClient();

  public OrderSteps(ScenarioState state) { this.state = state; }

  @When("they place an order worth {bigdecimal}")
  public void place(BigDecimal amount) {
    state.lastResponse = orders.place(state.customer.id(), amount);
  }
}

Writing scenarios people will read

Most Cucumber suites fail on the writing, not the tooling. Write declaratively: Given a gold member, not Given I open the login page and type [email protected]. Imperative, click-by-click steps tie the feature to the user interface, so every redesign rewrites the specification, and nobody outside QA reads them. Keep scenarios short, ideally three to five steps, with one When. Use Background only for context every scenario in the file needs. Prefer a Rule with two or three named examples over an outline with thirty rows of near-identical data, which belongs in a unit or parameterised test. Review new scenarios with a product owner before automating them; if they cannot follow it, rewrite it.

Running in GitHub Actions

Because Cucumber runs as a JUnit Platform engine, the pipeline is an ordinary Maven build. Cache dependencies through setup-java, run mvn verify, and upload the HTML report and the JUnit XML whether or not the build fails, so a red run can be investigated without re-running it. For pull requests, run a tagged subset such as @smoke by overriding cucumber.filter.tags on the command line, and run the full suite nightly. If the suite outgrows one machine, the simplest split is by tag or feature folder across matrix jobs, since Cucumber itself does not shard across machines.

# .github/workflows/acceptance.yml
name: acceptance
on: [push, pull_request]
jobs:
  cucumber:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-java@v6
        with:
          distribution: temurin
          java-version: '21'
          cache: maven
      # Pull requests run the smoke subset; pushes to main run everything
      - name: Run scenarios
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            mvn -B verify "-Dcucumber.filter.tags=@smoke and not @wip"
          else
            mvn -B verify
          fi
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: cucumber-report
          path: |
            target/cucumber.html
            target/cucumber.xml

Upgrading to Cucumber-JVM 8

Cucumber-JVM 8.0.0 was released on 24 September 2026, the first major in about five years, and 8.0.4 followed on 2 October. Most suites upgrade in an afternoon, but check five things. Run on Java 17 or later. If you use the html, json, messages, timeline or usage-json plugins, add Jackson yourself, since it is no longer shaded in. Move off cucumber-junit and cucumber-testng to the JUnit Platform engine, as both are deprecated for removal. Replace cucumber-openejb and cucumber-cdi2 with their Jakarta modules; cucumber-deltaspike is gone. And consider the new glue class name patterns, which skip loading unrelated classes during scanning and can speed up start-up noticeably on large projects.

<!-- pom.xml additions for Cucumber-JVM 8 -->
<dependency>
  <groupId>io.cucumber</groupId>
  <artifactId>cucumber-junit-platform-engine</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.platform</groupId>
  <artifactId>junit-platform-suite</artifactId>
  <scope>test</scope>
</dependency>
<!-- Jackson is opt-in from v8: needed by the html, json and messages plugins -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jdk8</artifactId>
  <scope>test</scope>
</dependency>

Pitfalls

Common pitfalls

Imperative, user-interface-level steps

Why it happens: Steps such as When I click the submit button are easy to write and reuse, so suites fill with them. The feature then describes the screen rather than the rule, product owners stop reading it, and every redesign breaks many scenarios at once.

The fix: Write steps at the level of business intent, such as When they place an order, and push clicks and selectors down into page objects or API clients called from the step definition.

Static fields to pass state between steps

Why it happens: It is the quickest way to share a value between two step classes. Static state survives into the next scenario and is shared across threads, so scenarios pass alone and fail in sequence or in parallel.

The fix: Use dependency injection: cucumber-picocontainer with a scenario state class in the constructor, or the Spring or Guice module. Each scenario then gets a fresh, isolated instance.

Following JUnit 4 era tutorials

Why it happens: Years of articles show @RunWith(Cucumber.class) with @CucumberOptions, TestNG runners and Java 8. In Cucumber-JVM 8 the JUnit 4 and TestNG modules are deprecated for removal, @Cucumber is deprecated in favour of @Suite, and Java 17 is the minimum.

The fix: Use cucumber-junit-platform-engine with a @Suite runner class and junit-platform.properties, as the official archetype generates, and move @CucumberOptions settings into configuration parameters.

HTML or JSON report plugins failing after the v8 upgrade

Why it happens: Cucumber-JVM 8 stopped shading Jackson. The html, json, messages, timeline and usage-json plugins need it, so a project that only bumped the version number loses its reports.

The fix: Add jackson-databind and jackson-datatype-jdk8 for Jackson 2, or tools.jackson.core:jackson-databind for Jackson 3, as test dependencies.

Step definitions with the same meaning written five ways

Why it happens: Nobody checks the existing vocabulary before writing a new step, so I am logged in as an admin, Given an admin user and the admin has signed in all exist with slightly different behaviour.

The fix: Agree a small domain vocabulary, review new steps in code review, and run the usage plugin or a dry run periodically to find unused and near-duplicate definitions to merge.

Scenario outlines used as data-driven test tables

Why it happens: Outlines are an easy way to add coverage, so they grow to dozens of rows that vary one field. The feature becomes unreadable and each row pays the full end-to-end cost.

The fix: Keep two or three examples that each illustrate a different rule, and move exhaustive data variation into a parameterised unit or API test.

Compared

How Cucumber compares

ToolStep code languagesRuns onLicenceExecuted throughReports
CucumberJava, Kotlin, JavaScript, TypeScript, Ruby, C++JVM, Node.js, RubyMITJUnit Platform, CLI or npm scriptHTML, JSON, JUnit XML, Cucumber Messages
SpecFlow / ReqnrollC#.NET Framework 4.6.2 and modern .NETBSD 3-Clause (Reqnroll)NUnit, MSTest, xUnit or TUnitHTML and Cucumber Messages formatters
BehavePythonPythonBSD 2-ClauseIts own behave commandJSON formatter and JUnit XML
Serenity BDDJava and other JVM languagesJVM, Java 17 or laterApache 2.0Cucumber or JUnit 5 and laterLiving documentation HTML site

FAQ

Frequently asked questions

What is Cucumber?

Cucumber runs specifications written in Gherkin, a structured plain-language format of Given, When and Then steps, by matching each step to a step definition in code. Official implementations cover Java and other JVM languages, JavaScript and TypeScript, Ruby and C++. It is free under the MIT licence and, since December 2024, owned by its open source community rather than a company.

Is Cucumber free?

Yes. The official implementations, Cucumber-JVM, cucumber-js and Cucumber-Ruby among them, are open source under the MIT licence, with no paid edition and no features held back. Since December 2024 the Cucumber name, trademarks and website belong to the Open Source Collective rather than SmartBear, and the project is funded by sponsorship. Your costs are the engineering time to write and maintain scenarios and step definitions, and the tools you pair it with.

What changed in Cucumber-JVM 8?

Released on 24 September 2026, v8 requires Java 17, adopts JSpecify nullability annotations, and makes Jackson an opt-in dependency for the html, json, messages, timeline and usage-json plugins. Step definitions and hooks can now be package private, except with PicoContainer. The JUnit 4 and TestNG runners are deprecated for removal in favour of the JUnit Platform engine, the Java EE modules were removed in favour of Jakarta, and glue scanning can be narrowed with class name patterns.

Should I use Cucumber if business people will not read the scenarios?

Usually not. The value of Cucumber is a shared, readable specification that stays executable. If only testers and developers will ever read the scenarios, the Gherkin layer adds a second artefact to maintain for every test without giving anyone new information. Write those tests directly in JUnit, pytest, Playwright or your API test tool, and keep Cucumber for the rules where a product owner genuinely takes part in agreeing examples.

Cucumber vs SpecFlow or Reqnroll: which should a .NET team use?

Reqnroll. Cucumber has no official .NET implementation, and SpecFlow reached end of life on 31 December 2024. Reqnroll is the community continuation built from the SpecFlow codebase, listed by cucumber.io as a semi-official implementation, and it uses the same Gherkin parser and Cucumber Expressions. Gherkin knowledge and feature files transfer directly; step definitions are written in C# with attributes instead of Java annotations.

How do I run Cucumber scenarios in parallel?

On the JVM, set cucumber.execution.parallel.enabled=true in junit-platform.properties, and choose the dynamic strategy, which uses the number of cores, or a fixed thread count. Scenarios must not share static state, and those that use a shared resource can be tagged and mapped to an exclusive resource lock. In cucumber-js, pass --parallel with a worker count; since v13 workers are threads rather than child processes, so they share memory with the main process.

Why are my steps undefined when the step definition exists?

Almost always the glue path. Cucumber only loads step definitions from the packages named in the glue property, so a class outside that package is invisible and its steps are reported as undefined with a suggested snippet. Check that cucumber.glue, or the GLUE_PROPERTY_NAME parameter on your suite class, names the package that holds the step classes, and that the step text matches the expression exactly, including parameter types.

Do undefined or pending steps fail the build?

Yes, in current versions. Cucumber-JVM has run in strict mode by default since v6, and v7 removed the --strict and --non-strict options, so undefined and pending steps fail the scenario. In cucumber-js the strict option defaults to true and fails the run on pending steps. Older articles that describe green builds with undefined steps refer to the earlier non-strict default.

Is Cucumber still worth learning in 2026?

Yes, if you work in teams that practise BDD or in regulated domains that value readable acceptance criteria, and because many existing suites need maintaining. The project is active: Cucumber-JVM 8, cucumber-js 13 and Cucumber-Ruby 12 all shipped in 2026. Learn it alongside a plain test framework, and learn the discipline of writing good scenarios, which matters more than the API.

Alternatives

Cucumber alternatives worth weighing

Resources

Where to go next

Keep going

Prepare, or get it done

Ready

Picking the wrong tool costs more than picking late.

QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.

Talk to QA Advisor