Resource library

QA How-To

Java for Testers: dependency injection with PicoContainer (2026)

Learn java testers dependency injection with PicoContainer for Cucumber, including scenario scope, constructor injection, shared context, cleanup, and testing.

26 min read | 2,485 words

TL;DR

For Cucumber-JVM, add `io.cucumber:cucumber-picocontainer`, give glue classes constructors that declare their collaborators, and let Cucumber create a fresh object graph per scenario. Share only scenario-owned data through a typed context, keep resources lazy, and clean them through injected After hooks.

Key Takeaways

  • Add `cucumber-picocontainer` on the test classpath and let Cucumber construct glue objects for each scenario.
  • Use constructor injection exclusively so every dependency is explicit, testable, and available before a step runs.
  • Separate immutable services from a small scenario context that stores only data created or observed by one scenario.
  • Keep browser and remote resources lazy because constructor injection should not make every scenario perform expensive setup.
  • Place cleanup in injected hooks and make it idempotent for partial setup, failed steps, and retries.
  • Avoid static contexts, service locators, circular graphs, and a single bag of untyped scenario data.
  • Choose PicoContainer for lightweight Cucumber glue unless the application already has a DI framework whose test integration adds real value.

The java testers dependency injection with PicoContainer approach gives Cucumber step definitions and hooks one scenario-scoped object graph without static fields. Add the Cucumber PicoContainer module, declare dependencies in constructors, and Cucumber creates fresh glue and shared collaborators for every scenario.

That simple model improves isolation, parallel safety, and unit testing, but only if the graph has clear responsibilities. This guide uses Cucumber-JVM 7.34.4 and Java 17, provides a runnable project structure, and explains context design, resource cleanup, failure diagnosis, and framework selection for 2026 QA automation.

TL;DR

Problem PicoContainer design Result
Two step classes need the same customer ID Inject one typed ScenarioContext Scenario-local state without statics
Steps need API behavior Inject an ApiClient Explicit, replaceable collaboration
Only UI scenarios need a browser Inject a lazy BrowserSession No browser starts in constructors
Hooks must clean test data Inject the context and client into hooks Ownership-aware teardown
Parallel scenarios run together Let Cucumber create each scenario graph No cross-scenario field sharing
Glue unit tests need fakes Call constructors directly No container required in unit tests

Do not instantiate a DefaultPicoContainer in ordinary Cucumber glue. The cucumber-picocontainer integration is the object factory and owns the scenario lifecycle.

1. What java testers dependency injection with PicoContainer Means

Dependency injection means an object receives its collaborators rather than finding or constructing them internally. In Cucumber Java, constructor injection lets a step class declare that it needs a customer API, scenario context, clock, or browser session. PicoContainer resolves those constructor dependencies and supplies instances when Cucumber creates glue.

Cucumber normally creates a new instance of every glue class before each scenario. Empty constructors need no additional module. Once glue classes depend on shared services or state, a supported DI module organizes that graph. Cucumber recommends PicoContainer when the application does not already use another dependency-injection framework. It is small, constructor-oriented, and does not require production annotations on test classes.

The most important behavior is scenario scope. Within one scenario, two glue classes requesting the same component type receive the component from that scenario's object graph. The next scenario receives a new graph. This is exactly what test isolation needs: steps can collaborate, while a customer ID from scenario A cannot leak into scenario B through an instance field.

PicoContainer does not fix poor boundaries. If ScenarioContext is a global static singleton, or if a service stores mutable state in static fields, the suite is still unsafe. DI makes ownership visible, but engineers must keep the graph scenario-local and distinguish services from per-scenario state.

2. Add the Correct Cucumber PicoContainer Dependencies

Keep Cucumber artifacts on one version. As of July 2026, the official Cucumber documentation shows 7.34.4 for cucumber-picocontainer. The current Cucumber line uses Java 17 as its baseline. This Maven configuration also adds the JUnit Platform engine and suite API for discovery.

<properties>
  <maven.compiler.release>17</maven.compiler.release>
  <cucumber.version>7.34.4</cucumber.version>
  <junit.platform.version>6.0.2</junit.platform.version>
</properties>

<dependencies>
  <dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>${cucumber.version}</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-picocontainer</artifactId>
    <version>${cucumber.version}</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-junit-platform-engine</artifactId>
    <version>${cucumber.version}</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-suite</artifactId>
    <version>${junit.platform.version}</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Do not add org.picocontainer:picocontainer merely to make Cucumber injection work. The Cucumber adapter brings the supported integration. Direct core PicoContainer APIs such as DefaultPicoContainer are useful for applications that intentionally manage a container, but normal Cucumber glue should let the Cucumber object factory control creation and disposal.

Keep cucumber-picocontainer in test scope. Run mvn dependency:tree if Cucumber artifacts drift to different versions through plugins or internal framework modules. An object-factory error can be a dependency convergence problem, not a constructor problem.

3. Build a Typed Scenario Context

A scenario context should contain values created or learned during one scenario, not every service in the framework. Use named fields and domain types instead of Map<String, Object>. A map postpones errors until runtime, hides ownership, and turns refactoring into string search.

package example.support;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public final class ScenarioContext {
    private String customerId;
    private final List<String> createdOrderIds = new ArrayList<>();

    public void rememberCustomer(String id) {
        customerId = Objects.requireNonNull(id, "id");
    }

    public String requireCustomerId() {
        if (customerId == null) {
            throw new IllegalStateException("No customer was created in this scenario");
        }
        return customerId;
    }

    public Optional<String> customerId() {
        return Optional.ofNullable(customerId);
    }

    public void rememberOrder(String id) {
        createdOrderIds.add(Objects.requireNonNull(id, "id"));
    }

    public List<String> createdOrderIds() {
        return List.copyOf(createdOrderIds);
    }
}

The API communicates state transitions. rememberCustomer records ownership, requireCustomerId fails near the first invalid step order, and customerId supports cleanup after partial setup. Returning a copied list prevents hooks from accidentally changing state while iterating.

Keep the context small. When payment, browser, orders, and messaging state grow independently, split them into CustomerScenario, BrowserSession, and MessageProbe. PicoContainer can inject multiple focused objects. A giant context becomes a service locator and allows unrelated steps to reach into each other's internals.

4. Constructor Injection Across Step Classes

Consider a feature in src/test/resources/features/customer.feature. It creates a customer in one step class and checks that customer's profile in another.

Feature: Customer profile

  @api
  Scenario: New customer has the standard plan
    Given a standard customer exists
    When the customer profile is requested
    Then the profile plan is STANDARD

The following client is an in-memory example so all Java code can run without inventing a remote service. A real project would implement the same boundary with its supported HTTP client.

package example.support;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public final class CustomerApi {
    private final AtomicInteger sequence = new AtomicInteger(1000);
    private final Map<String, Customer> customers = new HashMap<>();

    public Customer create(String plan) {
        String id = "customer-" + sequence.incrementAndGet();
        Customer customer = new Customer(id, plan);
        customers.put(id, customer);
        return customer;
    }

    public Customer get(String id) {
        Customer result = customers.get(id);
        if (result == null) {
            throw new IllegalArgumentException("Unknown customer: " + id);
        }
        return result;
    }

    public void deleteIfPresent(String id) {
        customers.remove(id);
    }

    public record Customer(String id, String plan) { }
}

One class creates the fixture. PicoContainer supplies both constructor arguments.

package example.steps;

import example.support.CustomerApi;
import example.support.ScenarioContext;
import io.cucumber.java.en.Given;

public final class CustomerCreationSteps {
    private final CustomerApi api;
    private final ScenarioContext context;

    public CustomerCreationSteps(CustomerApi api, ScenarioContext context) {
        this.api = api;
        this.context = context;
    }

    @Given("a standard customer exists")
    public void createStandardCustomer() {
        CustomerApi.Customer customer = api.create("STANDARD");
        context.rememberCustomer(customer.id());
    }
}

Another class reads the same scenario context and same API instance. No static variable or manual new CustomerCreationSteps(...) is required.

5. Complete Step and Runner Example

The profile steps store the observed response in their own field. That field belongs only to this glue object in this scenario. Cross-class data belongs in the typed context.

package example.steps;

import static org.junit.jupiter.api.Assertions.assertEquals;

import example.support.CustomerApi;
import example.support.ScenarioContext;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

public final class CustomerProfileSteps {
    private final CustomerApi api;
    private final ScenarioContext context;
    private CustomerApi.Customer observed;

    public CustomerProfileSteps(CustomerApi api, ScenarioContext context) {
        this.api = api;
        this.context = context;
    }

    @When("the customer profile is requested")
    public void requestProfile() {
        observed = api.get(context.requireCustomerId());
    }

    @Then("the profile plan is {word}")
    public void verifyPlan(String expectedPlan) {
        assertEquals(expectedPlan, observed.plan());
    }
}

The JUnit Platform suite points to features. Glue configuration lives in a property file.

package example.suite;

import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
public class RunCucumberTest {
}
cucumber.glue=example.steps
cucumber.plugin=pretty,html:target/cucumber-report.html

Run mvn test. Cucumber discovers glue classes, selects PicoContainer because its adapter is on the test classpath, resolves CustomerApi and ScenarioContext, and creates a fresh graph for the scenario. If the dependency module is missing, parameterized glue constructors cannot be satisfied by the default object factory.

This example uses Jupiter assertions inside steps. The package remains org.junit.jupiter.api in the current JUnit programming model. Teams can use AssertJ or another supported assertion library instead, but assertions should report expected behavior clearly.

6. Inject Hooks for Cleanup and Evidence

Hooks are glue too, so PicoContainer can inject the same components. Cleanup reads optional state because a Given step might fail before recording an ID.

package example.steps;

import example.support.CustomerApi;
import example.support.ScenarioContext;
import io.cucumber.java.After;
import io.cucumber.java.Scenario;

public final class CleanupHooks {
    private final CustomerApi api;
    private final ScenarioContext context;

    public CleanupHooks(CustomerApi api, ScenarioContext context) {
        this.api = api;
        this.context = context;
    }

    @After("@api")
    public void deleteCreatedCustomer(Scenario scenario) {
        context.customerId().ifPresent(api::deleteIfPresent);
        scenario.log("Scenario-owned customer cleanup completed");
    }
}

A cleanup hook should be idempotent. Deleting an already absent record should succeed or return a known harmless result. If the real API returns 404 for a second delete, the client can classify that as already clean. Never truncate a shared table or delete by a broad date range. Track identifiers owned by the scenario and remove only those.

Evidence capture follows the same pattern. Inject a lazy browser session and a reporter or use Cucumber's Scenario.attach. Run screenshot logic before the browser close hook using explicit order values. Handle a browser that never started. The Cucumber hooks and tags guide covers ordering and conditional hooks in depth.

Keep cleanup failure visible, but preserve the original failure. Record both in the report where the framework allows it. A test that passes assertions but cannot clean its data should normally fail because it damages later isolation. A test already failing should still expose its assertion as the primary diagnosis with cleanup as secondary evidence.

7. Lazy Resources and Expensive Dependencies

Constructor injection should wire objects, not start remote systems. If BrowserSession launches Chrome in its constructor, every scenario whose graph includes the component may pay that cost, including API-only scenarios. Constructors should validate required collaborators and remain fast. Start the resource through a conditional hook or first-use method.

package example.support;

import java.util.Optional;
import org.openqa.selenium.WebDriver;

public final class BrowserSession {
    private WebDriver driver;

    public void start(WebDriver createdDriver) {
        if (driver != null) {
            throw new IllegalStateException("Browser already started");
        }
        driver = createdDriver;
    }

    public WebDriver requireDriver() {
        if (driver == null) {
            throw new IllegalStateException("Browser was not started");
        }
        return driver;
    }

    public Optional<WebDriver> driver() {
        return Optional.ofNullable(driver);
    }

    public void closeIfStarted() {
        if (driver != null) {
            try {
                driver.quit();
            } finally {
                driver = null;
            }
        }
    }
}

A tagged @Before("@ui") hook can ask a WebDriverFactory for a driver and pass it to start. An @After("@ui") hook calls closeIfStarted. The session owns one scenario's browser, while the factory owns browser-type selection and driver construction. This is clearer than injecting raw WebDriver everywhere.

The same lazy rule applies to message consumers, database transactions, mock servers, and temporary directories. Give each resource an explicit lifecycle and one owner. Do not rely on garbage collection or process shutdown for cleanup.

8. PicoContainer vs Manual Wiring, Spring, and Guice

The best DI choice follows the application's ecosystem and the test graph's needs.

Option Best fit Advantages Costs
PicoContainer Standalone Cucumber automation Minimal setup, constructor injection, scenario scope Few advanced binding features
Manual no-arg glue Small suite with no shared collaborators No DI dependency Encourages repeated construction as suite grows
Static scenario holder No valid fit Superficially easy Shared mutable state and parallel failures
Spring integration Application already uses Spring test context Reuses beans, profiles, configuration Heavier startup and context complexity
Guice integration Existing Guice modules and bindings Rich binding control More configuration than simple glue needs

PicoContainer is not automatically faster in every suite, and framework choice should not rest on fabricated benchmarks. Measure startup, memory, and maintenance on your graph. For a standalone UI or API suite, PicoContainer usually supplies the needed scenario composition with little ceremony.

When testing a Spring application, cucumber-spring may let glue reuse supported application beans and test configuration. Do not load a complete application context merely to inject three page objects. Conversely, do not duplicate a complicated production binding graph in PicoContainer if the application framework already provides a reliable test boundary.

Whichever module you choose, use only one Cucumber object factory unless you intentionally configure a custom one. Multiple DI adapters on the classpath can produce ambiguous selection or unexpected bootstrapping. Keep the choice documented in the framework README.

9. Diagnosing Constructor and Object Graph Failures

A PicoContainer failure usually points to one of five causes: a missing constructible dependency, a circular dependency, multiple unsupported choices for one type, a constructor that performs failing work, or mismatched Cucumber dependencies. Read the deepest cause, then draw the constructor graph from the failing glue class.

A cycle such as OrderSteps -> ScenarioContext -> OrderService -> OrderSteps signals a boundary mistake. Step classes should depend on services and state; services should never depend on step classes. Break the cycle by moving shared behavior into a dedicated collaborator. Do not hide it with a static accessor.

Interfaces require a binding strategy. For simple Cucumber Pico graphs, prefer injecting a concrete test adapter or one unambiguous implementation. If environment selection is needed, inject a concrete factory that reads validated configuration and returns the right client. Complex qualifiers and production module reuse may justify Spring or Guice instead of building a custom service locator.

Constructor exceptions are especially confusing because the scenario may fail before a step begins. Keep constructors free of network calls, browser startup, database migration, and file downloads. Validate configuration in a controlled global or scenario hook and report missing settings by name without printing secrets.

Finally, inspect mvn dependency:tree. Keep cucumber-java, engine, and DI adapter aligned. Remove legacy adapters and duplicate object-factory modules. A tiny smoke scenario with two injected step classes is an effective diagnostic fixture for framework upgrades.

10. Reviewing java testers dependency injection with PicoContainer

Review the graph from scope outward. Every mutable component should be owned by one scenario. Every process-wide component should be immutable and thread-safe, or avoided. No glue class should read a static driver, customer ID, response, or credentials holder. System properties and environment variables may supply immutable configuration, but parse and validate them once behind a typed settings object.

Next, inspect dependency direction. Step definitions and hooks depend on domain clients, page workflows, resource sessions, and scenario state. Those lower-level components never import step packages. A component should have one reason to change. Splitting a 40-field context is usually more valuable than shortening constructor lists with a locator.

Test glue logic without PicoContainer by calling constructors directly with fakes. This is a benefit of DI, not a workaround. Keep a small engine-level suite to verify discovery, object-factory selection, scope, tag hooks, and cleanup. A unit test proves behavior, while the engine test proves wiring.

For more Java framework design, see Java test automation framework architecture and test data builders for Java testers. The same principles recur: explicit ownership, deterministic construction, and side effects at visible boundaries.

Interview Questions and Answers

Q: Why use PicoContainer with Cucumber-JVM?

It provides lightweight constructor injection and a fresh object graph for each scenario. Step classes and hooks can share typed scenario state and services without static variables. It is the usual choice when the application does not already use another DI framework.

Q: How does scenario scope work?

Cucumber creates glue and injected components for one scenario, reuses those instances within that scenario's graph, then creates a new graph for the next scenario. Mutable fields therefore remain isolated if code does not escape them into statics or external shared resources.

Q: Should you manually create a PicoContainer?

Not for normal Cucumber glue. Adding cucumber-picocontainer lets Cucumber's object factory manage the container lifecycle. Manual container creation produces a second graph and defeats scenario scoping.

Q: What belongs in ScenarioContext?

Only typed values created or observed during one scenario, such as resource IDs or the last domain response. Services, page objects, configuration, and global caches should be separate collaborators.

Q: How do hooks access the same state as steps?

Hooks are glue classes, so their constructors can request the same context and clients. PicoContainer supplies the scenario's instances. Cleanup can then inspect optional identifiers and remove only owned resources.

Q: How do you test classes that use PicoContainer injection?

Constructor injection keeps tests simple. I instantiate the class directly with fakes and assert its behavior without starting a container. I keep a small Cucumber engine test for wiring and scope.

Q: Why should resource creation not happen in constructors?

Constructors may run during glue graph creation and before a scenario step provides useful context. Remote work there makes API-only scenarios expensive and turns setup failures into vague object-creation errors. Use explicit, conditional lifecycle hooks or lazy methods.

Q: When would Spring be better than PicoContainer?

Spring is a better fit when the tested application already exposes a supported Spring test context and reusing its beans or profiles reduces real duplication. For a small standalone Cucumber graph, PicoContainer is usually simpler.

Common Mistakes

  • Putting ScenarioContext, WebDriver, responses, or resource IDs in static fields.
  • Constructing step classes manually from other step classes.
  • Creating a DefaultPicoContainer inside hooks even though Cucumber owns the object factory.
  • Starting browsers or making HTTP calls inside constructors.
  • Storing all values in Map<String, Object> with string keys and casts.
  • Injecting one giant context instead of focused state and service components.
  • Creating circular dependencies between steps, services, and context.
  • Adding PicoContainer, Spring, and Guice adapters to the same test runtime accidentally.
  • Deleting broad environment data instead of scenario-owned identifiers.
  • Assuming DI makes external test accounts and files automatically parallel-safe.
  • Testing only mocks and never verifying Cucumber engine discovery and scope.

Conclusion

The java testers dependency injection with PicoContainer design is effective because it matches Cucumber's scenario boundary. Add the supported adapter, use explicit constructors, keep state typed and local, make resources lazy, and inject cleanup hooks into the same object graph.

Begin with two step classes that currently exchange data through a static field. Replace that field with a small scenario context, add constructor injection, and run the scenarios in parallel. Once scope is trustworthy, extend the graph through focused clients and resource owners rather than a universal context bag.

Interview Questions and Answers

What problem does PicoContainer solve in Cucumber?

It constructs glue classes and their dependencies within a scenario-scoped graph. This lets steps and hooks share typed collaborators without static state or manual wiring. The next scenario receives new mutable components, which supports isolation.

How do you share a customer ID between two step classes?

I create a typed `ScenarioContext` with methods to remember and require the ID, then inject it into both constructors. PicoContainer supplies the same context inside one scenario. I never use a static field.

What should not go into a Cucumber ScenarioContext?

It should not become a bag for services, configuration, page objects, and unrelated domain state. I keep services as separate dependencies and split independent scenario state into focused components. This preserves ownership and type safety.

How does cleanup work with injected dependencies?

An After hook requests the same context and clients through its constructor. It reads optional IDs recorded by the scenario and deletes only those resources. Cleanup is idempotent because setup may have failed before every resource existed.

Why avoid network calls in injected constructors?

Container construction occurs before useful scenario execution and may create dependencies that a scenario never uses. A network failure then appears as an object-creation problem. I keep constructors cheap and use conditional hooks or explicit lazy methods for I/O.

How do you diagnose a PicoContainer circular dependency?

I draw the constructor graph from the failing glue type and find the path back to an earlier component. Steps should depend on services and state, while those components should never depend on steps. Moving shared behavior to a lower-level collaborator breaks the cycle cleanly.

Should Cucumber glue create DefaultPicoContainer manually?

No. The Cucumber PicoContainer adapter is the object factory and owns one graph per scenario. A manually created container is a separate graph with the wrong lifecycle and usually leads to duplicate services or lost shared state.

How would you choose between PicoContainer and Spring for Cucumber?

I use PicoContainer for a lightweight standalone test graph. I choose Spring when a real Spring application context, profiles, or beans are already part of the test boundary and reusing them simplifies integration. I measure startup and maintenance rather than selecting by popularity.

Frequently Asked Questions

What is PicoContainer in Cucumber?

It is a supported Cucumber-JVM dependency-injection module that resolves glue constructors and manages a fresh object graph for each scenario. It is recommended when the application does not already use another DI framework.

How do I add PicoContainer to Cucumber Java?

Add `io.cucumber:cucumber-picocontainer` in test scope and use the same version as all other Cucumber artifacts. Then declare dependencies in glue constructors.

Can PicoContainer share state between step definitions?

Yes. Inject the same typed scenario context into each step class. Cucumber and PicoContainer provide the context instance for that scenario and a new instance for the next scenario.

Do I need annotations such as Inject with PicoContainer?

Ordinary Cucumber PicoContainer usage relies on constructor injection and does not require an injection annotation. Keep one clear constructor that declares the class's collaborators.

Should WebDriver be stored in ScenarioContext?

A focused `BrowserSession` component is usually clearer than putting a raw driver in a giant context. Make it scenario-scoped, start it only for UI scenarios, and close it idempotently in an injected hook.

Is PicoContainer thread-safe for parallel Cucumber tests?

Its scenario object graph supports isolation, but your own static fields and shared external resources can still race. Use unique data, scenario-owned files, and thread-safe process-level services.

When should Cucumber use Spring instead of PicoContainer?

Use Spring when the application already has a supported Spring test context and its beans or profiles are genuinely useful to the tests. PicoContainer is simpler for a standalone automation object graph.

Related Guides