Sharing Steps via a Base Class
Place common step implementations in a base class that multiple markers extend. The generator detects step methods already present anywhere in a marker's class hierarchy and reuses them — it emits no abstract declaration (or stub) for a step a base class already implements.
This example uses two feature files (ShoppingCart and Checkout) whose markers both extend the same BaseShopSteps, so the cart setup steps are implemented once and reused across both.
Source: examples/common-use-cases/example-3 on GitHub.
What this demonstrates
- Common step methods live in a reusable base class (
BaseShopSteps) - Two marker classes extend the base — the shared steps are reused across both feature implementations
- The generator declares abstract methods only for each feature's own steps
- Shared state (the cart) and helpers (
subtotal()) also live in the base class — no per-feature glue - Base-class inheritance as an alternative to interface composition (see Organizing Steps into Interfaces)
Class hierarchy
BaseShopSteps.java (shared cart steps + state + helpers)
├→ ShoppingCartFeature.java (marker → specs/ShoppingCart.feature)
│ └→ ShoppingCartScenarios.java (generated, abstract)
│ └→ ShoppingCartTest.java (concrete — cart-assertion steps)
└→ CheckoutFeature.java (marker → specs/Checkout.feature)
└→ CheckoutScenarios.java (generated, abstract)
└→ CheckoutTest.java (concrete — checkout steps)
Both features use Given I have an empty shopping cart and
When I add "…" with quantity "…" and unit price "…" — implemented once in BaseShopSteps.
The base class
Common steps, shared state, and helpers live here. Any marker that extends this class inherits them:
public abstract class BaseShopSteps {
protected final List<CartItem> cart = new ArrayList<>();
public void iHaveAnEmptyShoppingCart() {
cart.clear();
}
public void iAdd$p1WithQuantity$p2AndUnitPrice$p3(String name, Integer quantity, Double unitPrice) {
cart.add(new CartItem(name, quantity, unitPrice));
}
protected int itemCount() {
return cart.size();
}
protected double subtotal() {
return cart.stream()
.mapToDouble(item -> item.quantity() * item.unitPrice())
.sum();
}
private record CartItem(String name, int quantity, double unitPrice) {}
}
Two markers, two concrete tests
Each marker extends the base and points at its own feature; each concrete test implements only its feature's own steps:
@Gherkin2JUnit("specs/ShoppingCart.feature")
public abstract class ShoppingCartFeature extends BaseShopSteps {}
@Gherkin2JUnit("specs/Checkout.feature")
public abstract class CheckoutFeature extends BaseShopSteps {}
public class ShoppingCartTest extends ShoppingCartScenarios {
@Override
public void theCartShouldContain$p1Item(Integer expectedCount) {
assertEquals(expectedCount, itemCount());
}
@Override
public void theCartSubtotalShouldBe$p1(Double expectedSubtotal) {
assertEquals(expectedSubtotal, subtotal(), 0.001);
}
}
public class CheckoutTest extends CheckoutScenarios {
private double orderTotal;
@Override
public void iCheckOut() {
orderTotal = subtotal(); // reuses the base-class helper
}
@Override
public void theOrderTotalShouldBe$p1(Double expectedTotal) {
assertEquals(expectedTotal, orderTotal, 0.001);
}
}
What gets generated
Each generated …Scenarios class inherits the two setup steps from BaseShopSteps, so it declares only its own feature-specific steps as abstract. For example, CheckoutScenarios:
public abstract class CheckoutScenarios extends CheckoutFeature {
// iHaveAnEmptyShoppingCart() and iAdd$p1WithQuantity$p2AndUnitPrice$p3(...) are
// inherited from BaseShopSteps — no abstract declarations emitted for them.
public abstract void iCheckOut();
public abstract void theOrderTotalShouldBe$p1(Double p1);
@Test
@DisplayName("Scenario: Check out an order")
public void scenario_1() {
/*
* Given I have an empty shopping cart
*/
iHaveAnEmptyShoppingCart();
// … remaining step calls …
}
}
Run it
cd examples/common-use-cases/example-3
mvn test