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…Scenariosclass 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
Testinstead ofScenarios(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. The spec, ShoppingCart.specb, sits next to the marker in src/test/java, so a bare @Gherkin2JUnit finds it by convention:
@Gherkin2JUnitOptions(shouldBeAbstract = false)
@Gherkin2JUnit
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
| Aspect | Abstract (default) | Concrete |
|---|---|---|
| Generated class | abstract, extends marker | Concrete, extends marker |
| New (unimplemented) step methods | abstract declarations | Failing stubs (Assertions.fail(...)) |
| Inherited step methods | Not re-declared | Not re-declared |
| Missing implementations | Compile error | Runtime failure |
| Where you implement | Concrete subclass (or the marker class) | Marker class |
| Subclass required to run | Yes | No |
| Class suffix | Scenarios | Test |
| 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.
unimplementedStepBehaviorcontrols what an unimplemented stub does:FAIL(default) callsAssertions.fail(...),ABORTmarks the test as aborted viaAssumptions.assumeTrue(false, …), andCOMPILATION_ERRORemits invalid code so a missing step blocks compilation even in concrete mode.
Run it
cd examples/going-further/example-1
mvn test