Cucumber-JVM and JUnit 5: Dependency Injection State Management
0 reputation · 10 Jan 2023, 22:48 UTC
0 reputation · 10 Jan 2023, 22:48 UTC
When integrating Cucumber-JVM with the JUnit 5 platform, managing state between multiple step definition classes within a single scenario requires a Dependency Injection (DI) container. While Cucumber supports various containers such as PicoContainer and Spring, the behavior of object lifecycles differs across these implementations.
The primary goal is to ensure that shared state is instantiated once per scenario and disposed of immediately after the scenario completes to prevent state leakage between tests. However, uncertainty exists regarding the interaction between the JUnit 5 test lifecycle and the DI container's scope when utilizing complex nested configurations.
Cucumber-JVM—not the JUnit 5 engine—owns the DI container lifecycle. After each scenario (pass or fail), Cucumber core invokes ObjectFactory.stopScenario() in a finally block, which disposes scenario-scoped objects. JUnit 5’s @AfterEach does not run per Gherkin scenario, so it cannot be relied on for DI cleanup. Mixing cucumber-picocontainer and cucumber-spring on the same classpath is unsupported: both register an ObjectFactory, causing ambiguous container selection and potential duplicate step-definition instantiation. Choose one container for all step definitions in a test run.
ObjectFactory implementation. Including both PicoContainer and Spring modules makes which one wins non-deterministic.stopScenario(). The Cucumber runtime (not JUnit) calls this after every scenario, even when a step throws. PicoContainer drops its scenario-scoped instances; Spring closes the child ApplicationContext created for that scenario.cucumber-junit-platform-engine maps each scenario to a dynamic test node. @BeforeEach/@AfterEach fire once per test template (the feature file), not per scenario.stopScenario().cucumber-spring builds a parent ApplicationContext from @ContextConfiguration and a child context per scenario. stopScenario() closes the child context, invoking @PreDestroy and DisposableBean callbacks.@PreDestroy (Spring) or a no-arg close() method (PicoContainer) that logs a unique marker.throw new AssertionError()).stopScenario() executed.mvn dependency:tree | grep -E 'cucumber-picocontainer|cucumber-spring'—only one should be present.Use one DI container:
cucumber-spring alone with @ContextConfiguration.cucumber-picocontainer alone.What are your exact Cucumber-JVM and Spring Framework versions? The child-context close behavior and Jakarta/javax namespace shift between major versions can change whether stopScenario() reliably disposes beans on failure.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 11 Jan 2023, 05:28 UTC
When a scenario fails, Cucumber‑JVM still runs its @After hooks because the JUnit 5 extension treats each scenario as a test method and invokes the extension’s afterEach callback. This guarantees that the ObjectFactory disposes of any @Singleton or scenario‑scoped beans, so mutable state does not leak.
If you inject Spring beans into PicoContainer‑managed step definitions, remember that the Spring application context is not torn down per scenario. To avoid leakage, annotate the test class with @DirtiesContext or use @TestInstance(Lifecycle.PER_METHOD) to force a fresh context for each scenario.