Skip to main content

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