Skip to main content

Organizing Steps into Interfaces

Instead of declaring every step method on one class, group them into domain interfaces and have the marker class implement those interfaces. The generator inherits the step methods through the interfaces — no abstract declarations, no per-feature glue. The concrete test class only supplies the state the steps work on.

Source: examples/going-further/example-3 on GitHub.

What this demonstrates​

  • Step methods grouped by domain into interfaces (CartSteps, CheckoutSteps)
  • Interfaces carry shared implementations via Java default methods
  • Interfaces can't hold state, so each declares a cart() accessor; the steps work on whatever Cart it returns
  • The marker class implements the interfaces to pull in all their steps
  • The generator sees inherited step methods and emits no abstract declarations for them
  • A single marker can compose any number of step interfaces
  • The concrete ShoppingCartTest only supplies the state — one fresh Cart per test — and makes the scenarios runnable

The step-interface pattern​

Organise step methods by domain area using interfaces with default methods. An interface can't hold fields, so each one declares an abstract cart() accessor and its steps work on the Cart it returns:

public interface CartSteps {

Cart cart();

default void iHaveAnEmptyShoppingCart() {
cart().clear();
}

default void iAdd$p1ToTheCart(String item) {
cart().add(item);
}
}

public interface CheckoutSteps {

Cart cart();

default void iProceedToCheckout() {
cart().startCheckout();
}

default void iPayWithCard$p1(String cardNumber) {
cart().pay(cardNumber);
}

default void theOrderShouldBeConfirmed() {
assertTrue(cart().isOrderConfirmed(), "The order was not confirmed");
}
}

Both interfaces declare the same cart(), so cart and checkout steps share one cart. Cart is a tiny in-memory cart and checkout that ships with the example.

Then the marker class implements every interface. The spec sits next to it in src/test/java, so a bare @Gherkin2JUnit finds it by convention:

@Gherkin2JUnit
public abstract class ShoppingCartFeature implements CartSteps, CheckoutSteps {
}

What gets generated​

The generated ShoppingCartScenarios inherits all step methods through the marker, so none of them are re-declared as abstract:

public abstract class ShoppingCartScenarios extends ShoppingCartFeature {

// No abstract step methods — every step is inherited from
// CartSteps / CheckoutSteps via ShoppingCartFeature.

@Test
@DisplayName("Scenario: Add an item and check out")
public void scenario_1() {
/*
* Given I have an empty shopping cart
*/
iHaveAnEmptyShoppingCart();
// … remaining step calls …
}
}

The concrete test​

The only thing left abstract is cart(). The concrete test supplies it — one fresh Cart per test — and, by being concrete, makes the scenarios runnable. Without it the generated class stays abstract and JUnit never runs it:

public class ShoppingCartTest extends ShoppingCartScenarios {

private final Cart cart = new Cart();

@Override
public Cart cart() {
return cart;
}
}

The class hierarchy​

CartSteps, CheckoutSteps              (interfaces — default step implementations, abstract cart() accessor)
└→ ShoppingCartFeature.java (marker, @Gherkin2JUnit — implements both interfaces)
└→ ShoppingCartScenarios.java (generated, abstract — no step declarations, all inherited)
└→ ShoppingCartTest.java (your concrete class — implements cart())

Why organize this way​

  • Cohesion — cart steps live with cart steps, checkout steps with checkout steps
  • Reuse — the same interface can back many markers (pair this with Glob Patterns to share one vocabulary across many features)
  • Composition — mix and match interfaces per marker instead of one monolithic step class
  • State stays in the test — the interfaces hold only behaviour, so each test gets its own state and the steps stay reusable

Run it​

cd examples/going-further/example-3
mvn test