Skip to main content

Migrating from Cucumber

Your .feature files carry over unchanged — SpecBinder reads the same Gherkin. What changes is everything around them: there is no runner, no glue code, and no project-wide catalogue of step definitions.

What goes away

In CucumberIn SpecBinder
A runner class, or the JUnit Platform Cucumber engineNothing — the generated test is an ordinary JUnit 5 class
@CucumberOptions (glue packages, features path, plugins)Nothing — you point at spec files with @Gherkin2JUnit
Step definitions found by scanning the classpathStep methods on the class you annotate, or on anything it extends or implements
Regular expressions matching step text to methodsMethod names derived from the step text at compile time
Missing step definitions found when the test runsMissing steps are compile errors

Step definitions

In Cucumber a step definition can live anywhere on the classpath and is found by matching its pattern against the step text. In SpecBinder each spec file gets its own generated class, and steps are matched by method name, derived from the step's wording.

Given I have an empty shopping cart

becomes a call to iHaveAnEmptyShoppingCart(). Where you write that method depends on whether the step belongs to this one spec or is shared between several.

A step used by this spec only. By default SpecBinder generates an abstract class from your spec, carrying one abstract method per step. You write a concrete class extending it, and that is where the step's real code lives:

public class ShoppingCartTest extends ShoppingCartScenarios {
@Override
public void iHaveAnEmptyShoppingCart() { /* … */ }
}

Leave one unimplemented and the project doesn't compile — that is how an undefined step becomes a compile error.

You don't work the names out yourself. Build the project and SpecBinder declares an abstract method for every step, with the name and parameter types already derived from the Gherkin. Your IDE can then create the matching empty methods in your test class — in IntelliJ, Implement methods on the class — leaving you to write only the bodies.

A step shared across specs. Implement it above the generated class instead: on the class you annotated, on a class that class extends, or on an interface it implements as a default method. SpecBinder finds it there and never declares it abstract, so none of your concrete test classes has to implement it. Interfaces let you group steps by area, and a class can pull in as many as it needs — often the better fit for a suite that already had step definitions spread across several files — see Organizing Steps into Interfaces.

Keeping your @Given / @When / @Then annotations

Your existing Cucumber step methods — the ones annotated with @Given, @When or @Then — are the part of a migration you least want to touch, because they hold the actual test logic. Cucumber found each of them by the pattern in its annotation, so their method names could be anything at all. Under SpecBinder's default name-based matching those names would not match, so they would have to be renamed to whatever SpecBinder derives from their step text. Only the names would change, not the code inside them — quick enough across a handful of steps, but a lot of churn for no behaviour change across a large suite.

You don't have to rename anything. SpecBinder can match on those annotation patterns instead, leaving your existing methods exactly as they are.

To switch that on, add a second annotation, @Gherkin2JUnitOptions, with useCucumberAnnotationsForStepMatching set to true:

@Gherkin2JUnit("specs/ShoppingCart.specb")
@Gherkin2JUnitOptions(useCucumberAnnotationsForStepMatching = true)
public abstract class ShoppingCartFeature { }

Both regular expressions and Cucumber expressions work as the pattern inside your @Given / @When / @Then annotations. Cucumber expressions cover the built-in parameter types — {int}, {string}, {word} and so on — but custom parameter types are not supported. Where a step relies on one, do the conversion inside the step method instead.

There is also the addCucumberStepAnnotations option, which does the opposite — it writes @Given / @When / @Then onto the generated methods. You don't supply the patterns: each one is generated from the step's own wording. It needs cucumber-java on the test classpath.

The two options are independent, and both default to off.

Hooks and setup

Cucumber's @Before and @After hooks have no direct equivalent, because the generated class is plain JUnit 5. Use JUnit's own lifecycle annotations instead — @BeforeEach, @AfterEach, @BeforeAll, @AfterAll. They can go on the class you annotate or anywhere in its hierarchy, since JUnit inherits them, so setup needed by several specs lives on a shared parent exactly as shared step methods do.

A Gherkin Background: is generated for you as a @BeforeEach method, so background steps need no special handling.

Cucumber's tagged hooks (@Before("@slow")) have no equivalent. Where you relied on those, either put the logic in a Background: or use a JUnit extension.

Shared state

Cucumber needs a World object or a dependency-injection container (PicoContainer, Spring) to carry state between step definitions, because those definitions live in unrelated classes.

In SpecBinder the steps of a scenario are all methods on one object, so state can simply be a field.

@Gherkin2JUnit("specs/ShoppingCart.specb")
public abstract class ShoppingCartFeature {
private final List<CartItem> cart = new ArrayList<>(); // that's the whole mechanism
}

JUnit creates a fresh instance per test by default, so scenarios don't leak state into each other.

Tags

Gherkin tags become JUnit @Tag annotations on the generated class, nested class, or method, so you filter them with your build tool's ordinary JUnit tag filtering rather than with Cucumber's tag expressions. See Tags for the Maven and Gradle syntax.

Note that empty scenarios are tagged @new automatically, so that tag participates in filtering too.

Data tables and doc strings

A data table becomes a typed row class by default — one field per column, with types inferred, so a renamed column shows up as a compile error.

If your steps already use Cucumber's DataTable API and you would rather not rewrite them, you can keep it:

@Gherkin2JUnit("specs/ShoppingCart.specb")
@Gherkin2JUnitOptions(dataTableParameterType = CUCUMBER_DATA_TABLE)
public abstract class ShoppingCartFeature { }

This requires a getTableConverter() method somewhere in the class hierarchy. LIST_OF_MAPS is also available if you want the plain map form.

Doc strings arrive as a trailing String parameter on the step method, emitted as a Java text block.

Reports

Cucumber's plugin-based reporters (html, json, junit) don't apply. Because the tests are ordinary JUnit 5, whatever already reports on your JUnit tests keeps working.

For Gherkin-level results, add the execution reporter — a separate dependency plus @ExtendWith(SpecBinderReporter.class) on the class you annotate. It writes one JSON file per spec, under target/specbinder-reports/ with Maven or build/specbinder-reports/ with Gradle, mirroring the spec's own folders:

target/specbinder-reports/specs/ShoppingCart.feature.json

Each file describes the spec as it ran: its Rules, scenarios and Scenario Outline example rows, and every step with its original Gherkin text, typed arguments, status and timing. A failed step also carries the assertion message, the expected and actual values, and the stack trace.

That level of detail is there because this report is what the IntelliJ plugin reads to bring results back into the spec file itself — pass/fail icons in the gutter beside each Feature, Rule and Scenario; an inline panel on a failed step showing message, expected, actual and stack trace; diff viewers for the two values; and a one-click Approve that writes the actual value straight into the step in the spec file. Switch that on under IntelliJ IDE settings: Settings → Tools → SpecBinder → Execution results.

Migrating gradually

You don't have to convert everything at once. SpecBinder only processes the spec files you point it at, so Cucumber and SpecBinder can live side by side in the same project for as long as you need.

A single test run then covers both. Your Cucumber runner carries on executing the spec files you haven't moved yet, exactly as before, while the ones you have converted are ordinary JUnit 5 classes that the plain JUnit runner picks up alongside every other test. Neither knows about the other, so you can move one spec across at a time and keep the suite green throughout.