Skip to main content
Back to Blog
BDD
2026-09-28

JBehave vs Cucumber: Java BDD Frameworks Compared

JBehave vs Cucumber comparison for Java QA teams choosing BDD syntax, JUnit 5 support, reporting, migration effort, agent workflows, and upgrade risks.

JBehave vs Cucumber: Java BDD Frameworks Compared

JBehave vs Cucumber in 2026 is no longer a neutral popularity contest. Cucumber-JVM is the safer default for most new Java BDD work because it is actively releasing, its official Java installation docs list Cucumber-JVM 8.0.2, and its changelog shows recent 8.0.x releases in September 2026 with Java 17 as the baseline. JBehave is still usable and documented, but its stable reference site identifies version 5.2.0 published on 25 September 2023, so a new adoption needs a stronger reason.

That does not mean every JBehave suite should be rewritten. JBehave remains a mature Java BDD framework with story files, annotated steps, JUnit entry points, Maven execution, reporting, Spring support, and a style that some legacy enterprise teams know well. The decision is really about risk: new framework choice, existing suite maintenance, JUnit 5 expectations, reporting pipeline, step-library shape, and how much help you expect from AI coding agents.

If you are starting fresh, pick Cucumber-JVM unless your organization has a large JBehave platform already. If you are maintaining a stable JBehave suite, keep it while you evaluate dependency health and Java version plans. If you are migrating, move feature by feature, preserve the business language first, and use the migration as a chance to delete vague steps rather than translate them mechanically.

Verified Status in 2026

The official signals are uneven. Cucumber's Java installation page is current, lists io.cucumber:cucumber-java:8.0.2, and points JUnit 5 users to cucumber-junit-platform-engine. The Cucumber-JVM changelog lists 8.0.2 on 2026-09-25, 8.0.1 on 2026-09-24, and 8.0.0 on 2026-09-24, including changes such as a Java 17 baseline and deprecation of older JUnit and TestNG adapters in favor of the JUnit Platform engine.

JBehave's official stable reference pages list JBehave 5.2.0, published on 25/09/2023. Its docs still describe stable Maven artifacts, snapshots, JUnit-runnable entry points, an Embedder core, reporting formats, and modules such as Spring, Guice, Pico, Groovy, Scala, and Gherkin. That is enough for maintenance, but it is not the same activity profile as Cucumber-JVM.

AreaJBehaveCucumber-JVM2026 implication
Current official version signalStable docs show 5.2.0 from 2023Java docs and changelog show 8.0.2 in September 2026Cucumber has stronger freshness for new suites
Primary text format.story files with JBehave grammar.feature files using GherkinCucumber is more familiar to cross-language teams
JUnit 5 storyJBehave docs expose JupiterStories in API docsOfficial JUnit Platform engine is the forward pathCucumber integrates more naturally with modern JUnit Platform builds
Java baselineConfirm in your dependency graph before upgradingCucumber-JVM 8 sets baseline to Java 17Cucumber 8 may require runtime upgrades in older shops
Community recognitionEstablished but quieterLarge Cucumber ecosystem across languagesAI tools and new hires usually recognize Cucumber faster

For broader BDD framework selection, pair this comparison with BDD frameworks comparison. For a hands-on Cucumber workflow, the companion BDD Cucumber testing guide goes deeper into project layout, tags, and executable scenarios.

Syntax and Business Readability

JBehave and Cucumber both aim to express behavior in business-readable text, but their defaults shape teams differently. JBehave story files use narrative sections such as Narrative:, As a, I want to, and So that, followed by scenarios and steps. Cucumber feature files use Feature, Rule, Scenario, Scenario Outline, Examples, and tags from Gherkin.

Feature: Password reset

  Rule: A reset link is valid for one account

    @smoke @email
    Scenario: Request a reset link for a known email
      Given a registered customer exists with email "nora@example.test"
      When the customer requests a password reset
      Then a reset email is queued for "nora@example.test"
      And the reset token can be used once
Narrative:
As a registered customer
I want to request a password reset
So that I can recover access without contacting support

Scenario: Request a reset link for a known email
Given a registered customer exists with email nora@example.test
When the customer requests a password reset
Then a reset email is queued for nora@example.test
And the reset token can be used once

The biggest syntax difference is not the words. It is the surrounding ecosystem. Product owners, QA engineers, and AI tools tend to recognize Gherkin feature files quickly because Cucumber popularized the format and many other tools use it. JBehave stories are readable, but a team often needs local conventions documented more explicitly.

Syntax questionJBehave answerCucumber answerDecision guidance
Will non-Java stakeholders recognize the file?Often, after explanationUsually yes if they have seen GherkinPrefer Cucumber for cross-functional review
Can examples drive parameterized scenarios?Yes, with examples tablesYes, with Scenario Outline and ExamplesBoth work, choose by existing style
Are tags first-class in workflows?Meta filters support filteringTags are central and widely documentedCucumber is simpler for CI tag lanes
Is grammar extensibility needed?JBehave offers deep configurationCucumber favors common Gherkin conventionsJBehave fits teams with custom story semantics

What people get wrong: BDD syntax does not rescue unclear examples. "Given user logs in" is weak in either framework if authentication state is the precondition and no role, account state, or security boundary is specified. A good AI coding agent can generate glue for vague steps, but that only automates ambiguity faster.

Step Binding and Code Shape

JBehave step classes use annotations from org.jbehave.core.annotations, often with regex-like patterns or parameter placeholders. Cucumber-JVM step classes use io.cucumber.java.en.Given, When, Then, and Cucumber Expressions or regular expressions. Both can produce clean code or a landfill of reusable-but-meaningless steps.

package com.example.reset;

import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;

import static org.assertj.core.api.Assertions.assertThat;

public class PasswordResetSteps {
    private final ResetHarness harness = new ResetHarness();

    @Given("a registered customer exists with email $email")
    public void registeredCustomerExists(String email) {
        harness.createCustomer(email);
    }

    @When("the customer requests a password reset")
    public void customerRequestsReset() {
        harness.requestReset();
    }

    @Then("a reset email is queued for $email")
    public void resetEmailIsQueued(String email) {
        assertThat(harness.queuedEmails()).contains(email);
    }
}
package com.example.reset;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

import static org.assertj.core.api.Assertions.assertThat;

public class PasswordResetSteps {
    private final ResetHarness harness;

    public PasswordResetSteps(ResetHarness harness) {
        this.harness = harness;
    }

    @Given("a registered customer exists with email {string}")
    public void registeredCustomerExists(String email) {
        harness.createCustomer(email);
    }

    @When("the customer requests a password reset")
    public void customerRequestsReset() {
        harness.requestReset();
    }

    @Then("a reset email is queued for {string}")
    public void resetEmailIsQueued(String email) {
        assertThat(harness.queuedEmails()).contains(email);
    }
}

Cucumber Expressions such as {string}, {int}, and custom parameter types are usually easier for new contributors than raw regex patterns. JBehave's parameter style can be concise too, but many legacy suites drift into permissive patterns that match too much. The framework is rarely the root cause. Step ownership is.

Step design issueGood patternJBehave cautionCucumber caution
Shared stateScenario-scoped harness or dependency injectionAvoid static fields in step classesAvoid global objects outside scenario scope
ParametersStrong names and typed conversionDo not write patterns that match every sentenceDo not overuse anonymous expression placeholders
AssertionsAssert observable domain effectsPending steps can hide unfinished behavior if toleratedUndefined steps should fail CI immediately
ReuseReuse domain helpers, not vague language"Given data exists" becomes a junk drawer"Given I do stuff" is still useless in Gherkin

For AI coding agents, Cucumber's annotation style and public examples are more likely to be reproduced correctly from model memory. With JBehave, give the agent a local exemplar file and forbid inventing runner classes or annotation names. That prompt detail can be the difference between compiling steps and plausible-looking code.

Runner and JUnit Platform Integration

Cucumber-JVM's modern recommendation is the JUnit Platform engine for JUnit 5 builds. The official docs still mention cucumber-junit for JUnit 4, but the 8.0 changelog deprecates older JUnit and TestNG adapters for removal in favor of cucumber-junit-platform-engine. For new Java projects, that is the key fact.

<project>
  <dependencies>
    <dependency>
      <groupId>io.cucumber</groupId>
      <artifactId>cucumber-java</artifactId>
      <version>8.0.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.cucumber</groupId>
      <artifactId>cucumber-junit-platform-engine</artifactId>
      <version>8.0.2</version>
      <scope>test</scope>
    </dependency>
    <!-- Enables constructor injection of shared state such as ResetHarness into step classes -->
    <dependency>
      <groupId>io.cucumber</groupId>
      <artifactId>cucumber-picocontainer</artifactId>
      <version>8.0.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <version>3.27.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
cucumber.glue=com.example.reset
cucumber.plugin=pretty, html:target/cucumber-report.html, json:target/cucumber-report.json
cucumber.filter.tags=not @manual

JBehave's official running stories documentation describes support for running stories as JUnit tests, command-line builds that support JUnit tests, other unit testing frameworks, Maven goals, and the Embedder as the core entry point. Its API docs include JUnitStory, JUnitStories, and JupiterStories, which matters for JUnit 5 users. In practice, many existing JBehave suites still carry custom runner classes.

package com.example.reset;

import java.util.List;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.junit.JupiterStories;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.InstanceStepsFactory;

import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML;

public class ResetStories extends JupiterStories {
    @Override
    public Configuration configuration() {
        return new MostUsefulConfiguration()
            .useStoryReporterBuilder(new StoryReporterBuilder()
                .withDefaultFormats()
                .withFormats(CONSOLE, HTML));
    }

    @Override
    public InjectableStepsFactory stepsFactory() {
        return new InstanceStepsFactory(configuration(), new PasswordResetSteps());
    }

    @Override
    public List<String> storyPaths() {
        return List.of("com/example/reset/password_reset.story");
    }
}

The Cucumber version is less custom. The JUnit Platform discovers scenarios through the engine and configuration properties. The JBehave version is explicit and flexible, but there is more Java code for agents and maintainers to keep correct.

Filtering, Tags, and CI Lanes

Cucumber tag expressions are a major reason teams standardize on it. Official docs show Maven filtering with -Dcucumber.filter.tags="@smoke" and environment-variable support through CUCUMBER_FILTER_TAGS. With the JUnit Platform engine, configuration comes through JUnit Platform properties and system properties, so tag lanes fit naturally into Maven or Gradle jobs.

name: java-bdd

on:
  pull_request:
  push:
    branches: [main]

jobs:
  cucumber-smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-java@v6
        with:
          distribution: temurin
          java-version: '21'
      - name: Run smoke scenarios
        run: mvn test -Dcucumber.filter.tags="@smoke and not @manual"
      - name: Upload Cucumber reports
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: cucumber-reports-${{ github.run_id }}
          path: target/cucumber-report.*

For JBehave, filtering is usually built around meta filters and runner or Maven plugin configuration. That can be powerful in established suites, but it is less universally recognized. If an AI agent needs to create a new smoke lane, Cucumber's property is easier to discover and review.

CI concernJBehave approachCucumber approachPractical result
Smoke laneMeta filters or story path selectioncucumber.filter.tagsCucumber is easier to express in one command
Report artifactStoryReporterBuilder formats or Maven configurationPlugins such as pretty, html, json, junitBoth work, Cucumber output is more commonly consumed
ParallelizationDepends on runner and build setupJUnit Platform and build-tool parallel settingsCucumber aligns with modern JUnit Platform work
Failed rerunsCustom setup often neededRerun file and plugin patterns are commonCucumber has more shared examples

Do not split BDD jobs only by framework capability. Split by risk and cost. Run smoke scenarios on pull requests, run full browser or service-backed BDD nightly if they are slow, and keep pure service-level scenarios in the normal test lane when they finish quickly.

Reporting and Debuggability

JBehave has long supported multiple reporting formats through StoryReporterBuilder, including console, text, HTML, XML, and statistics-oriented views. Its reporting documentation discusses Freemarker view generation resources and report views under target/jbehave/view. Existing enterprise suites may have built useful dashboards on that output.

Cucumber's reporting story is broader in the ecosystem. The cucumber.plugin property can produce pretty console output, HTML, JSON, JUnit XML, message streams, and third-party reporting inputs depending on the plugin set. Many CI dashboards and test management integrations already understand Cucumber JSON or JUnit XML.

For QA teams, debuggability comes from scenario scope and artifact quality, not from a prettier report. A report should answer four questions quickly: which scenario failed, which step failed, what assertion failed, and what state artifact is available. For API or service BDD, attach request and response bodies with secrets redacted. For browser BDD, attach screenshots, console logs, trace files, and server logs.

package com.example.reset;

import io.cucumber.java.After;
import io.cucumber.java.Scenario;

public class ScenarioArtifacts {
    private final ResetHarness harness;

    public ScenarioArtifacts(ResetHarness harness) {
        this.harness = harness;
    }

    @After
    public void attachDiagnostics(Scenario scenario) {
        if (!scenario.isFailed()) {
            return;
        }

        String events = String.join(System.lineSeparator(), harness.auditEvents());
        scenario.attach(events, "text/plain", "audit-events.txt");
    }
}

This is where AI agents help. Ask the agent to improve diagnostics for the failing scenario, not to rewrite the test. Good diagnostics reduce flaky reruns, speed triage, and make future agent fixes safer because the failure contains real evidence.

Spring and Dependency Injection

Both frameworks can integrate with Spring, but Cucumber-JVM's dependency injection modules are more visible in current official docs. The Cucumber Java installation page strongly recommends adding a dependency injection module when sharing state between step definitions, specifically to avoid static variables. The Cucumber state docs describe object factories, Spring, Guice, and scenario-scoped state patterns.

JBehave has Spring modules and Javadoc packages for Spring integration. If your existing test platform already wires JBehave steps through Spring, keep that design while checking dependency compatibility. New teams should compare the amount of custom bootstrap required. The less framework-specific magic in the test harness, the easier it is for both humans and agents to reason about failures.

package com.example.reset;

import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.boot.test.context.SpringBootTest;

@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CucumberSpringConfiguration {
}

With Spring, the danger is over-integration. A BDD test that starts the whole application, reaches a real database, talks to a real queue, and checks one validation message is expensive and hard to diagnose. Use Spring where it proves wiring or service behavior. Keep pure language and rule scenarios below that layer.

Migration From JBehave to Cucumber

Migration should not start with a global search-and-replace from .story to .feature. Start with inventory. Count story files, unique step patterns, runner classes, reports consumed by CI, meta filters, Spring configuration, pending steps, and flaky scenarios. Then choose a pilot area with high maintenance pain and moderate business value.

Migration unitLow-risk approachAvoid
Story filesConvert one capability at a timeMixing syntax in one scenario
Step librariesPort domain helpers first, steps secondCopying vague step names exactly
Tags and filtersMap meta filters to Cucumber tags in a tableChanging CI selection silently
ReportsProduce old and new reports during transitionRemoving artifacts teams still use
Data setupMove setup to explicit fixtures or APIsKeeping hidden static state
JBehave meta to Cucumber tag mapping

+----------------+-------------------+------------------------------+
| JBehave meta   | Cucumber tag      | Notes                        |
+----------------+-------------------+------------------------------+
| smoke          | @smoke            | Runs on pull request         |
| manual         | @manual           | Excluded from automation     |
| requiresEmail  | @requires-email   | Runs only in full test lane  |
+----------------+-------------------+------------------------------+

During migration, keep assertions meaningful. For example, if the old JBehave step only checked HTTP 200, strengthen it to assert the persisted side effect, emitted event, or visible state. A migration that preserves weak assertions produces a shinier but not safer suite.

AI agents can help with repetitive translation, but only after you provide examples. Give the agent one converted scenario, one converted step class, the tag mapping, and the dependency injection pattern. Ask for a small batch, compile it, run it, then review language quality. Do not allow a one-shot conversion of hundreds of stories without executable checkpoints.

Decision Matrix for Java QA Teams

Choose Cucumber-JVM for new work when your team wants mainstream Gherkin, JUnit Platform integration, active releases, broad reporting compatibility, and easier AI assistance. Choose JBehave when you already have a stable investment, custom story grammar, established JBehave reports, or team expertise that outweighs ecosystem freshness.

SituationRecommendationReason
New Java BDD project on Java 17 or 21Cucumber-JVM 8Current docs, active changelog, JUnit Platform direction
Existing JBehave suite with low churnKeep JBehave, monitor dependenciesMigration cost may exceed benefit
Existing JBehave suite with high flake and poor reportingPilot Cucumber migrationModernize language, tags, and diagnostics together
Product team already writes GherkinCucumber-JVMLower communication cost
Heavy custom story runner already builtJBehave or gradual migrationReplacing runner behavior may be expensive
AI coding agents generate most test codeCucumber-JVMMore examples and recognizable conventions

One final point: BDD is not a replacement for unit, API, contract, accessibility, performance, or exploratory testing. It is a collaboration format and an executable acceptance layer. If scenarios become technical scripts that only automation engineers can read, both JBehave and Cucumber have failed the same way.

Frequently Asked Questions

Is JBehave discontinued?

The official JBehave stable reference still exists and documents version 5.2.0, published on 25 September 2023, with core modules, JUnit entry points, Maven execution, reporting, and integration modules. That is not the same as a clear discontinuation notice. It is, however, a quieter maintenance signal than Cucumber-JVM, whose official docs and changelog show 8.0.2 in September 2026. Treat JBehave as maintainable for existing suites, but require a specific business reason before choosing it for a new one.

Should new Java teams choose Cucumber or JBehave?

Most new Java teams should choose Cucumber-JVM. It has current official Java installation docs, active Cucumber-JVM releases, JUnit Platform support, familiar Gherkin syntax, and a larger ecosystem of examples. JBehave can still be a rational choice when a company already has deep JBehave infrastructure, custom story conventions, or reports that would be costly to replace. For greenfield work, Cucumber usually creates less onboarding and tooling friction.

Can JBehave stories be migrated automatically to Cucumber features?

Some syntax translation can be automated, but a safe migration is not fully mechanical. You need to map metadata to tags, replace runner configuration, port step bindings, rebuild dependency injection, preserve reports, and improve weak assertions. AI coding agents can help with small batches if you provide a converted example and run tests after each batch. Avoid bulk conversion that compiles only after hundreds of files have changed.

Does Cucumber-JVM 8 require Java 17?

The Cucumber-JVM changelog for 8.0.0 says the project baseline was set to Java 17. That makes Java version planning part of the framework decision. Teams already on Java 17 or 21 should be fine, subject to normal dependency checks. Teams pinned to Java 8 or 11 need either an older Cucumber-JVM line, a Java upgrade, or a different strategy. Confirm the runtime in CI before changing dependencies.