Skip to main content

Concrete Mode

Concrete mode is the opt-out from the default. Instead of an abstract …Scenarios class you extend, the generator emits a concrete, directly-runnable …Test class. Unimplemented steps become failing stubs at run time, not compile errors.

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

The default is abstract mode (shouldBeAbstract = true), shown throughout the earlier examples: an abstract …Scenarios class with one abstract method per step, enforced at compile time. This page is about opting out of that.

What this demonstrates

  • @Gherkin2JUnitOptions(shouldBeAbstract = false) generates a concrete class
  • The generated class suffix is Test instead of Scenarios (configurable)
  • No subclass required — the generated class is runnable on its own
  • Step methods are implemented directly in the marker class; the generated class inherits them
  • Unimplemented steps become failing stubs (Assertions.fail(...)) — a runtime failure, not a compile error
  • Methods already implemented on the marker class are inherited and not stubbed

The class hierarchy

Concrete mode collapses the three-layer hierarchy of abstract mode into two layers — there is no hand-written subclass:

ShoppingCartFeature.java    (marker class, @Gherkin2JUnit + shouldBeAbstract = false, implements steps)
└→ ShoppingCartTest.java (generated, concrete, runnable — inherits your implementations)

The marker class

Implement the step methods directly on the marker class. Abstract mode is the default, so you opt into concrete mode explicitly:

@Gherkin2JUnitOptions(shouldBeAbstract = false)
@Gherkin2JUnit("specs/ShoppingCart.specb")
public abstract class ShoppingCartFeature {

private 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));
}

// … remaining steps …

record CartItem(String name, int quantity, double unitPrice) {}
}

Generated output

The generated ShoppingCartTest is concrete (note: no abstract keyword) and inherits the step methods you implemented on the marker. Any step you did not implement is emitted as a failing stub instead of an abstract declaration:

@DisplayName("Feature: ShoppingCart")
@Generated("dev.specbinder.processor.AnnotationProcessor")
public class ShoppingCartTest extends ShoppingCartFeature {

// A step NOT implemented on the marker class becomes a failing stub:
public void someUnimplementedStep() {
Assertions.fail("Step is not yet implemented");
}

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

Because the class is concrete, JUnit can instantiate and run it directly — no subclass needed. The trade-off: a forgotten step only surfaces when the test runs (the stub calls Assertions.fail(...)), whereas abstract mode would refuse to compile.

Abstract vs concrete mode

AspectAbstract (default)Concrete
Generated classabstract, extends markerConcrete, extends marker
New (unimplemented) step methodsabstract declarationsFailing stubs (Assertions.fail(...))
Inherited step methodsNot re-declaredNot re-declared
Missing implementationsCompile errorRuntime failure
Where you implementMarker class or a concrete subclassMarker class
Subclass required to runYesNo
Class suffixScenariosTest
Opt in via(default)@Gherkin2JUnitOptions(shouldBeAbstract = false)

In both modes, methods already present on the marker class are inherited — the generator emits neither a stub nor an abstract declaration for them. The difference is only in how new (unimplemented) steps are handled, and whether a subclass is needed to run the tests.

Tuning the stub behavior. unimplementedStepBehavior controls what an unimplemented stub does: FAIL (default) calls Assertions.fail(...), SKIP marks the test as skipped via Assumptions.assumeTrue(false, …), and COMPILATION_ERROR emits invalid code so a missing step blocks compilation even in concrete mode.

Run it

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