In Cucumber-JVM with PicoContainer, how do two step definition classes share one state object?
answer
- One shared object, no annotations anywhere
- The container builds the step classes
- Ask for it as a constructor parameter
- One instance per type, per scenario
- Container stops when the scenario ends
basics
~20 sBoth step definition classes declare the shared type as a constructor parameter. With cucumber-picocontainer present, PicoContainer builds the glue classes per scenario, creating one instance of that type and injecting the same reference into every class that asks.
solid answer
~40 sAdd the `cucumber-picocontainer` module to the test classpath and Cucumber stops instantiating glue classes with a no-argument constructor and starts building them through PicoContainer instead. You then write a plain class — say `HearingContext` — with no annotation of any kind, and declare it as a **constructor parameter** in every step definition class that needs it. Within one scenario, PicoContainer creates exactly one `HearingContext` and passes that same reference to `DocketSteps`, `ConflictSteps` and anything else that asks for it, so a value written by a `Given` in one class is visible to a `Then` in another. The container is created and disposed around each scenario, so the shared instance dies when the scenario ends and the next scenario starts from a fresh one. Field injection does nothing here; only constructor parameters are resolved.
code
java · 35 lines// Plain state holder - no annotations, no interface
public class HearingContext {
private String caseNumber;
public void bookCase(String caseNumber) { this.caseNumber = caseNumber; }
public String caseNumber() { return caseNumber; }
}
// One step definition class writes to it
public class DocketSteps {
private final HearingContext hearing;
public DocketSteps(HearingContext hearing) {
this.hearing = hearing;
}
@Given("case {string} is booked into courtroom 4B")
public void caseIsBooked(String caseNumber) {
hearing.bookCase(caseNumber);
}
}
// A different class reads the same instance back
public class ConflictSteps {
private final HearingContext hearing;
public ConflictSteps(HearingContext hearing) {
this.hearing = hearing;
}
@Then("the clerk is warned about a conflict for that case")
public void clerkIsWarned() {
assertNotNull(hearing.caseNumber());
}
}go deeper
Recall that adding cucumber-picocontainer lets step definition classes take a shared state object as a constructor parameter, with no annotation to write. Being able to name the pattern is enough at this level.
Explain the mechanics: a container per scenario, one instance per type inside it, greedy constructor selection, and the fact that the instance is discarded when the scenario ends rather than at the end of the feature file.
Show you understand the cost side. Per-scenario lifetime means expensive construction repeats hundreds of times, and only one object factory may be on the classpath. Be ready to say when you would move to Spring or Guice instead.
Own the standard for a large suite: which container, what shape the scenario-scoped objects take, and where the line falls between per-scenario state and shared immutable infrastructure. Mixing containers across teams is a failure you should be able to argue against.
## The mechanism Cucumber-JVM does not construct step definition classes itself when a dependency-injection module is present. Instead it delegates to an **object factory**, and `cucumber-picocontainer` supplies one backed by PicoContainer. Putting that module on the test classpath is the entire setup — there is no configuration file, no annotation and no module class to write. From then on, for each scenario: 1. Cucumber starts a fresh container. 2. It asks the container for every glue class that owns a step the scenario runs. 3. PicoContainer inspects each class's constructor, resolves the parameter types by instantiating them too, and caches one instance **per type** for the life of that container. 4. When the scenario finishes, Cucumber stops the container and everything it built becomes garbage. Step three is the part that answers the question. Because the container caches one instance per type, two step definition classes that both declare `HearingContext` in their constructors receive **the same object**, not two copies. That is the whole state-sharing story on the JVM: a plain object, passed by the container, scoped to one scenario. ```java public class HearingContext { private String caseNumber; private final List<String> conflicts = new ArrayList<>(); public void bookCase(String caseNumber) { this.caseNumber = caseNumber; } public String caseNumber() { return caseNumber; } public List<String> conflicts() { return conflicts; } } ``` `DocketSteps` takes it in its constructor and writes to it; `ConflictSteps` takes it in its constructor and reads from it. Neither class knows the other exists, which is exactly the decoupling you want when a courtroom-scheduling suite grows past a handful of feature files. ## What PicoContainer requires PicoContainer resolves dependencies by **greedy constructor injection**: it picks the constructor with the most parameters that it can actually satisfy. That imposes a few rules worth stating in an interview: - The shared class must be **concrete**. An interface with no registered implementation cannot be instantiated, and the run fails while building the glue. - Every constructor parameter type must itself be resolvable, recursively. A shared object that needs a configured base URL usually reads it from a system property or an environment variable in its own constructor rather than taking a `String` parameter, because Pico has no way to supply an arbitrary string. - **No cycles.** If `A` takes `B` and `B` takes `A`, the container cannot build either. - **No annotations.** There is nothing to add; a candidate who insists an injection annotation is required has not used it. - Field injection is not performed. A field declared but never assigned stays `null`, and the failure surfaces as a `NullPointerException` in the first step that touches it. ## Where the instance dies | Boundary | Does the shared instance survive? | | --- | --- | | Between two steps of one scenario | Yes — same container, same instance | | Between two step definition classes | Yes — one instance per type per scenario | | Between two scenarios | **No** — a new container, a new instance | | Between the rows of an `Examples` table | **No** — each row is its own scenario | | Across a whole feature file | **No** — the file is not a lifetime boundary | The practical consequence is that anything genuinely expensive should not live in a container-managed object. If a login handshake costs real time and is repeated for every one of a few hundred scenarios, that cost is now paid a few hundred times. Keep truly global, read-only setup out of the per-scenario graph and let the scenario-scoped object hold a reference to it. The equally important consequence is the one candidates get wrong in the other direction: **the state does not leak**. If a scenario passes in isolation but fails in a suite, a Pico-managed object is not the culprit; a static field is. ## When PicoContainer is not the right container PicoContainer is the default choice because it needs no configuration, and for a suite whose step definitions talk to an HTTP API it is usually enough. You outgrow it when the objects you need are already managed elsewhere: - **Spring** — the application under test is a Spring application and the steps want its real beans, a `TestRestTemplate` or a repository. `cucumber-spring` bootstraps the test context and makes glue classes beans. - **Guice** — the production code is already wired with Guice modules and you want the same bindings, with scenario-scoped bindings for mutable state. Whichever you pick, pick **one**. Two DI modules on the test classpath give Cucumber two object factories and it refuses to guess; you either drop a dependency or name the factory you want through Cucumber's `cucumber.object-factory` configuration option. Mixing them "so both styles work" is a common cause of a suite that will not start at all.
- What happens if the shared class's constructor needs an interface that has no implementation on the classpath?PicoContainer cannot instantiate it, so building the glue for that scenario fails before any step runs. The fix is to depend on a concrete class, or to have the shared object construct its own collaborator internally. Pico only resolves types it can build itself; it has no bindings file where you could map an interface to an implementation.
- A step definition class has both a no-argument constructor and a one-argument constructor. Which does PicoContainer use?PicoContainer is greedy: it picks the constructor with the most parameters it can satisfy, so the one-argument constructor wins whenever that parameter type is resolvable. This surprises people who added the no-argument constructor as a fallback and then wondered why their shared object was still injected. Keep exactly one constructor on glue classes to remove the ambiguity.
- A login handshake in a container-managed object costs real time on every scenario. What do you do?Recognise that per-scenario lifetime is the cause and move the expensive, genuinely shared work out of the per-scenario object graph — a lazily initialised, immutable holder that the scenario-scoped object merely references. Anything mutable must stay per scenario, or you have reinvented the shared-state bug the container removes.
saying these in an interview costs you the question
- Claims PicoContainer needs an injection annotation on the constructor
- Expects the shared instance to survive into the next scenario
- Uses a field for injection and wonders why it is null
- Puts two DI modules on the test classpath at once
- Declares the shared object static so it is found
- Thinks each step definition class gets its own copy