Dependency Injection Concepts in Cucumber

What Is Dependency Injection?

Dependency Injection, usually called DI, is a design pattern where an object receives the other objects it needs from an external source instead of creating them by itself. In Java, a dependency is any object that another class needs to complete its responsibility. A Cucumber step definition may need a page object. A page object may need a WebDriver instance. A business service may need an API service. A validator may need scenario context. Dependency Injection provides these required objects from outside the class.

The main idea is to separate object usage from object creation. A class should focus on its own work, not on building everything it depends on. When a class creates all of its dependencies using new, it becomes tightly coupled to concrete implementations. When the dependency is supplied from outside, the class becomes easier to reuse, easier to test, easier to replace, and easier to manage.

Object
  -> Creates Dependency

Object
  -> Receives Dependency

In simple terms, Dependency Injection means providing an object with the resources it needs instead of forcing that object to create those resources itself. In Cucumber automation, this becomes very important because a real framework contains step definitions, page objects, WebDriver managers, API clients, services, scenario context, reporting helpers, configuration readers, and utilities. If every class creates its own objects, the framework becomes difficult to control.

Why Dependency Injection Matters in Automation

Automation frameworks grow quickly. A small project may begin with one feature file and one step definition. Soon it has many features, multiple pages, reusable workflows, API setup calls, configuration files, reports, logs, screenshots, and parallel execution. Without a clear object-management strategy, classes start creating dependencies wherever they need them. This leads to duplicated object creation and inconsistent lifecycle management.

public class LoginSteps {
    LoginPage page = new LoginPage();
}

This looks harmless, but it creates tight coupling. LoginSteps now decides exactly how LoginPage is created. If LoginPage later needs a WebDriver, configuration object, wait utility, or logger, the step definition must know about those details. If another step class needs the same page object, it may create another instance. If tests run in parallel, object sharing can become unsafe. If a unit test needs a mock page object, replacing the dependency becomes difficult.

With Dependency Injection, the step definition asks for the object it needs. The framework or DI container creates the object and supplies it. The step definition becomes cleaner because it uses LoginPage without controlling its construction.

public class LoginSteps {
    private final LoginPage page;

    public LoginSteps(LoginPage page) {
        this.page = page;
    }
}

This design improves maintainability. It also prepares the framework for larger needs such as shared scenario context, reusable services, test doubles, driver injection, and scenario-scoped object lifecycles.

What Is a Dependency?

A dependency is any object a class requires to perform its job. In a Cucumber Selenium framework, LoginSteps may depend on LoginPage. In an API framework, CustomerSteps may depend on CustomerService. A CustomerService may depend on CustomerApiClient. A page object may depend on WebDriver. A reporting helper may depend on scenario information.

LoginSteps
  -> Needs
  -> LoginPage

CustomerService
  -> Needs
  -> CustomerApi

Dependencies are normal. Every useful class collaborates with other classes. The design question is not whether dependencies exist. The question is how they are provided and managed. If a class constructs all dependencies internally, it controls too much. If dependencies are supplied from outside, the framework can manage creation, replacement, lifecycle, and sharing more consistently.

A Real-Life Analogy

A simple way to understand Dependency Injection is to think about a restaurant kitchen. A chef needs an oven to cook. Without DI, the chef builds the oven before cooking. That is clearly not the chef's responsibility. With DI, the restaurant provides the oven, and the chef uses it to prepare food. The chef focuses on cooking because the required equipment is supplied by the environment.

Without DI:
Chef
  -> Builds Oven
  -> Starts Cooking

With DI:
Restaurant
  -> Provides Oven
  -> Chef Uses Oven

Automation classes work the same way. A step definition should not be responsible for constructing WebDriver, page objects, services, context objects, configuration readers, and validators. It should focus on mapping Gherkin behavior to framework actions. The framework or DI container should provide the objects it needs.

Dependency Injection Flow

In a Cucumber framework, the DI flow usually begins when a scenario starts. Cucumber identifies the step definition classes required for the scenario. A DI container creates those step definition objects and resolves their constructor parameters. If LoginSteps needs LoginPage, the container creates or retrieves a LoginPage. If LoginPage needs WebDriver, the container resolves that too. The step definition receives a fully prepared object graph.

Framework
  -> Creates Objects
  -> Injects Dependencies
  -> Step Definitions
  -> Business Logic

This flow allows object management to be centralized. Instead of each step definition deciding how dependencies are created, the DI layer manages construction rules. This is especially useful when the same object should be shared within a scenario but not shared across scenarios. Cucumber DI integrations commonly manage objects at scenario scope to keep tests isolated.

Tight Coupling

Tight coupling occurs when a class directly depends on a specific implementation and controls how that implementation is created. In automation, this often appears when step definitions use new repeatedly. The class becomes locked to one concrete type, one constructor, and one object lifecycle.

public class LoginSteps {
    LoginPage page = new LoginPage();
}

The immediate problem is that the class cannot easily use a different implementation. It cannot easily receive a mock object during testing. It cannot easily share the same context with another step class. It cannot participate cleanly in a framework-managed lifecycle. The class knows too much about object creation.

Tight coupling also spreads construction logic. If ten step classes create LoginPage, then a constructor change must be updated in ten places. If page objects later require WebDriver, timeout settings, or logging helpers, many classes must change. This is exactly the kind of maintenance problem Dependency Injection reduces.

Loose Coupling

Loose coupling means a class depends on what it needs but does not control the details of creation. The dependency is provided from outside, usually through a constructor. The class can use the object, but it does not decide how to build it.

public class LoginSteps {
    private final LoginPage page;

    public LoginSteps(LoginPage page) {
        this.page = page;
    }
}

This design makes the class easier to understand because its dependencies are visible in the constructor. It also makes the class easier to test because a test can provide a fake or mocked dependency. It makes the framework easier to maintain because object creation is centralized. Loose coupling does not mean there are no dependencies. It means dependencies are expressed cleanly and managed intentionally.

Types of Dependency Injection

Java applications commonly use three forms of Dependency Injection: constructor injection, setter injection, and field injection. All three provide dependencies from outside the class, but they differ in clarity, safety, and maintainability.

Dependency Injection
  -> Constructor Injection
  -> Setter Injection
  -> Field Injection

Constructor injection is generally preferred for required dependencies. Setter injection can be useful for optional dependencies. Field injection is simple but less explicit because dependencies are hidden inside the class fields. In automation frameworks, constructor injection is usually the cleanest option because it clearly shows what each step definition, page object, or service needs.

Constructor Injection

Constructor injection provides dependencies through the class constructor. The object cannot be created unless its required dependencies are supplied. This makes the design explicit. If a step definition needs a page object and scenario context, the constructor shows that dependency immediately.

public class LoginSteps {
    private final LoginPage page;
    private final ScenarioContext context;

    public LoginSteps(LoginPage page, ScenarioContext context) {
        this.page = page;
        this.context = context;
    }
}

Constructor injection supports immutable references because fields can be marked final. It also prevents partially initialized objects. A class cannot exist without the objects it needs. This is why constructor injection is widely recommended in modern Java design and is a strong fit for Cucumber step definitions.

Setter Injection

Setter injection provides dependencies through setter methods after the object is created. It can be useful when a dependency is optional, changeable, or not required for every execution path. However, it also allows the object to exist before all dependencies are set, which can lead to runtime errors if the setter is forgotten.

public class LoginSteps {
    private LoginPage page;

    public void setPage(LoginPage page) {
        this.page = page;
    }
}

In Cucumber automation, setter injection is less common than constructor injection. It may be used in specific frameworks or for optional collaborators, but required dependencies are usually clearer when placed in the constructor. If a dependency is necessary for the class to work, constructor injection communicates that requirement better.

Field Injection

Field injection places an annotation directly on a field and lets the DI framework assign the value. It can look clean because there is no constructor code, but it hides the class dependencies. A reader must inspect fields to understand what the class needs. It can also make unit testing harder because dependencies are not supplied through normal constructor parameters.

public class LoginSteps {
    @Inject
    LoginPage page;
}

Field injection is common in some frameworks because it is quick to write, but constructor injection is usually a better design choice for required dependencies. In test automation, explicit dependencies are valuable because framework classes are shared and maintained by many people. Hidden dependencies make the code harder to reason about.

Dependency Injection in Cucumber

Cucumber creates step definition objects during scenario execution. Without a DI integration, object sharing between step classes can become awkward. With Dependency Injection, step definitions can receive shared scenario objects, page objects, services, API clients, validators, and helpers through constructors.

Feature File
  -> Step Definition
  -> Injected Objects
  -> Page Objects
  -> Selenium

DI is useful because Cucumber scenarios often span multiple step definition classes. One class may handle login steps, another may handle customer steps, and another may handle order validation. These classes may need to share scenario-specific data such as a customer ID, authentication token, created order number, or current user role. A scenario context object can be injected into each step class so they share data safely within the same scenario.

Sharing Objects Between Steps

Object sharing is one of the most practical uses of DI in Cucumber. Suppose a scenario creates a customer in one step and verifies that customer in another step. The created customer ID must be available across step definitions. A shared ScenarioContext object can store the ID during the scenario. DI ensures each step class receives the same context instance for that scenario.

LoginSteps
  -> ScenarioContext
  -> CustomerSteps

This is better than using global static variables. Static data can leak across scenarios, especially during parallel execution. Scenario-scoped dependency injection keeps data isolated. Each scenario gets its own context instance, so values from one scenario do not accidentally affect another scenario.

Driver Injection

WebDriver is one of the most important dependencies in Selenium automation. A weak framework often creates the driver directly in many places. This causes duplicated setup, inconsistent browser configuration, and parallel execution problems. Dependency Injection can provide WebDriver through a driver manager or factory.

WebDriver
  -> Injected Into
  -> Page Object

When WebDriver is injected, page objects do not need to create browsers. They simply use the driver they receive. Driver setup can be centralized in a driver factory, hook, or framework configuration class. This makes browser selection, headless mode, remote execution, grid execution, and cleanup easier to manage.

Driver injection must be scoped carefully. In Cucumber, the WebDriver instance is usually scenario-scoped. One scenario gets one driver instance, and that driver is closed during teardown. Parallel scenarios should not share the same driver object.

Page Object Injection

Page objects are strong candidates for injection. A step definition should not need to know how to construct every page class. The DI container can create page objects and provide them to step definitions. If the page object needs WebDriver, the container can also resolve that dependency.

LoginSteps
  -> LoginPage
  -> WebDriver

Page object injection keeps step definitions clean. The step method can call loginPage.loginAs(user) instead of creating the page, finding the driver, setting waits, and then performing actions. This makes step definitions easier to read and closer to the business language used in the feature file.

Service Injection

Business services and API services can also be injected. In larger frameworks, step definitions should not directly contain REST Assured calls or complex business flows. A CustomerSteps class can receive a CustomerService, and that service can receive an CustomerApiClient or request builder.

CustomerSteps
  -> CustomerService
  -> CustomerApi

This layered design keeps responsibilities clean. Step definitions map behavior. Services coordinate workflows. API clients make HTTP requests. Validators check results. DI connects these pieces without forcing each class to construct the next one manually.

Scenario Context Injection

Scenario context is a shared object used to store data during one scenario execution. It may hold values such as authentication token, user ID, customer ID, order ID, response object, selected test data, or temporary file path. DI is a clean way to provide this context to every step class that needs it.

Scenario Context
  -> Token
  -> Customer ID
  -> Order ID
  -> API Response

The key requirement is scenario isolation. The context should be created fresh for each scenario. It should not be shared across the whole test run unless that is explicitly intended. Scenario-scoped context prevents data leakage, supports parallel execution, and makes tests more reliable.

Dependency Injection Containers

A DI container is a framework component that creates objects, resolves dependencies, manages lifecycle, and injects required objects into classes. Instead of manually wiring every object, the container builds the object graph. Popular Java DI options include PicoContainer, Spring, Guice, and CDI.

DI Container
  -> Creates Objects
  -> Resolves Constructor Parameters
  -> Manages Lifecycle
  -> Injects Dependencies

In Cucumber JVM projects, PicoContainer, Spring, and Guice are commonly used. The best choice depends on project complexity and the existing technology stack. A lightweight automation-only framework may use PicoContainer. A project already using Spring may prefer Spring integration. A project built around Guice may continue using Guice.

PicoContainer in Cucumber

PicoContainer is a lightweight DI container often used with Cucumber because it requires minimal configuration. It supports constructor injection and scenario-scoped object sharing. When Cucumber sees step definitions with constructor parameters, PicoContainer can create the required objects and provide them automatically.

Cucumber
  -> PicoContainer
  -> Create Objects
  -> Inject Dependencies

PicoContainer is popular in test automation because it keeps the setup simple. Teams can introduce DI without adopting a large application framework. For many Cucumber Selenium and REST Assured projects, this is enough. It helps step definitions share context, page objects, services, and helpers cleanly.

Spring Dependency Injection

Spring provides a powerful Dependency Injection container and is common in enterprise Java applications. If the application or test framework already uses Spring, Cucumber can integrate with the Spring context. Step definitions and supporting classes can be managed as Spring beans.

Cucumber
  -> Spring Context
  -> Bean
  -> Injection

Spring is useful when tests need existing application configuration, profiles, beans, or service clients. It is more feature-rich than PicoContainer, but it may also be heavier. Teams should choose Spring when its capabilities are needed, not simply because it is popular. For small automation projects, a lighter option may be easier to maintain.

Guice Dependency Injection

Guice is a Java dependency injection framework from Google. It uses modules to define bindings between interfaces and implementations. In a Cucumber framework, Guice can create step definitions, inject services, manage context objects, and support modular framework design.

Cucumber
  -> Guice
  -> Module
  -> Injection

Guice is useful when the project already uses Guice or when the team prefers explicit binding modules. It can support larger frameworks with clear dependency configuration. Like Spring, it should be selected when it aligns with the project architecture and team skill set.

Object Lifecycle

Object lifecycle defines how long an injected object lives. In Cucumber automation, many objects should live only for one scenario. A WebDriver instance, scenario context, response holder, or temporary data object should usually be created when the scenario starts and cleaned up after the scenario ends.

Scenario Starts
  -> Objects Created
  -> Scenario Executes
  -> Objects Destroyed

Scenario-level lifecycle prevents cross-scenario contamination. If two scenarios share the same context object, one scenario may read values created by another scenario. If two parallel scenarios share the same driver, browser actions can interfere with each other. Correct lifecycle management is one of the biggest practical benefits of DI in Cucumber.

Enterprise Framework Design

In an enterprise automation framework, DI sits near the framework core. Cucumber executes feature files through runners. The DI container creates step definitions and injects services, page objects, context objects, utilities, drivers, API clients, validators, and reporting helpers. Each layer receives what it needs without manually constructing the entire object graph.

Feature File
  -> Runner
  -> DI Container
  -> Step Definitions
  -> Business Services
  -> Page Objects / API Services
  -> Utilities
  -> Application

This design supports clean layering. Step definitions remain thin. Page objects stay focused on UI behavior. API services stay focused on HTTP behavior. Utilities remain reusable. Context remains scenario-scoped. Reports and logs receive the right scenario information. The DI container connects the framework without forcing tight coupling between all classes.

Benefits of Dependency Injection

Dependency Injection promotes loose coupling, better maintainability, easier testing, easier mocking, reusable components, cleaner code, centralized object management, and better scalability. These benefits matter strongly in Cucumber frameworks because many classes participate in one scenario execution.

DI also improves readability. Constructor parameters show exactly what a class needs. A reviewer can look at a step definition constructor and understand its dependencies immediately. This is much clearer than hidden object creation scattered inside methods. DI also supports change. If the implementation of a dependency changes, the consuming class may not need to change at all.

Common Mistake: Creating Objects Everywhere

The most common mistake is using new everywhere inside step definitions. This produces duplicated object creation and tight coupling. It also makes object lifecycle unclear. If one step creates a page object and another step creates a different instance, shared state may be lost. If each class creates its own service, configuration, or driver, consistency becomes difficult.

new LoginPage();
new CustomerService();
new ScenarioContext();

Using new is not always wrong. Some simple value objects can be created directly. But framework components such as page objects, services, context, drivers, and shared helpers are usually better managed through DI. The goal is not to ban object creation completely. The goal is to centralize object management where it improves design.

Common Mistake: Injecting Too Many Dependencies

A constructor with too many dependencies is a warning sign. If a step definition needs ten services, five page objects, three utilities, and two contexts, the class probably has too many responsibilities. DI makes dependencies visible, which helps expose this design problem.

The solution is usually refactoring. Split the class by business area, move workflows into service classes, group related behavior, or introduce a focused helper. Constructor injection is useful because it makes this problem obvious. Hidden field injection or manual object creation may hide the same issue until the class becomes difficult to maintain.

Common Mistake: Global Static Objects

Static global objects are often used as a shortcut for sharing state. A framework may define public static WebDriver driver or public static ScenarioContext context. This seems easy at first, but it becomes dangerous with parallel execution and independent scenarios. Static mutable state can leak values across tests.

public static WebDriver driver;
public static String customerId;

DI provides a cleaner alternative. A scenario-scoped driver manager can provide the correct driver. A scenario-scoped context object can hold data for only the current scenario. This reduces random failures and makes the framework safer for CI execution.

Common Mistake: Mixing Business Logic with Object Creation

Object creation should not be mixed with business logic. A step definition should not create drivers, page objects, API clients, and validators while also executing the business workflow. This makes the step long and hard to maintain. The DI container or framework setup should manage object creation, while the step definition uses the prepared objects.

When object creation is separated, code becomes easier to read. The business flow is visible. The construction details are centralized. Changes to object creation do not require changes to every scenario implementation.

Common Mistake: Sharing Objects Across Scenarios

Some objects should not be shared across scenarios. Scenario context, WebDriver, response holders, temporary test data, and generated IDs should usually be isolated. Sharing these objects globally can cause one scenario to depend on another scenario's state. This breaks test independence.

Scenario-scoped DI solves this by creating fresh objects for each scenario. If the framework runs ten scenarios, each scenario gets its own scenario-specific dependencies. This is critical for reliable parallel execution and repeatable test results.

Dependency Injection and Page Object Model

Dependency Injection works naturally with the Page Object Model. Page objects usually need WebDriver and sometimes wait utilities, JavaScript helpers, or configuration values. Instead of constructing those dependencies inside each page, the framework can inject them. Step definitions can then receive page objects ready to use.

This reduces boilerplate code and keeps page objects consistent. It also supports page object reuse. If a page object is needed by several step classes, the DI container can provide it without duplicated construction logic. If a page object constructor changes, the container wiring handles the update instead of many step classes changing manually.

Dependency Injection and REST Assured

REST Assured frameworks also benefit from Dependency Injection. API services may depend on request specifications, authentication helpers, configuration readers, payload builders, validators, and scenario context. Injecting these dependencies keeps API services focused on endpoint behavior.

For example, a CustomerApiService can receive an authenticated request specification and a JSON utility. It does not need to know how environment variables are loaded or how tokens are generated. This makes API automation easier to maintain and easier to adapt across environments.

Dependency Injection and Reporting

Reporting components often need access to scenario names, screenshots, logs, request details, response details, and failure information. DI can provide reporting helpers or scenario objects to hooks and services. This keeps evidence capture consistent without forcing every class to know reporting internals.

When reporting dependencies are managed properly, failed scenarios can attach screenshots, API responses, logs, and validation messages reliably. This is especially useful in enterprise teams where reports are reviewed by developers, testers, managers, and release owners.

Dependency Injection and Parallel Execution

Parallel execution increases the importance of Dependency Injection. Each scenario should receive independent objects where needed. WebDriver should not be shared globally. Scenario context should not be static. File names should be unique. Reports should attach evidence to the correct scenario. DI helps enforce this separation by creating and injecting scenario-scoped dependencies.

A framework that uses static shared objects may work in sequential execution but fail unpredictably in parallel. DI prepares the framework for scalable execution. It makes object ownership clearer and reduces accidental sharing.

Dependency Injection and Testability

DI improves testability because dependencies can be replaced. A service can be tested with a mock API client. A validator can be tested with a sample response. A step definition can be tested with a fake service. A configuration-dependent class can receive a test configuration object. This is much harder when classes create dependencies internally.

Automation frameworks are often not unit-tested as heavily as application code, but shared framework components still benefit from testability. If an API service, utility, or context class has important logic, DI makes it easier to test that logic independently.

Choosing a DI Framework

The best DI framework depends on the project. PicoContainer is lightweight and simple for Cucumber-specific automation projects. Spring is powerful and useful when the application or framework already uses Spring. Guice is a good fit for teams already using Guice or wanting explicit module-based bindings. CDI may fit Jakarta EE environments.

FrameworkCommon Usage
PicoContainerLightweight Cucumber projects
SpringSpring-based applications and test frameworks
GuiceProjects already using Google Guice
CDIJakarta EE style applications

Do not choose a heavy DI solution only because it sounds enterprise-level. Choose the simplest tool that solves the framework's object-management problem. A clean PicoContainer setup may be better than an overconfigured Spring setup for a small team. A Spring setup may be better when tests need access to existing Spring beans and profiles.

DI Best Practices

Prefer constructor injection for required dependencies. Keep classes focused so constructors do not become too large. Use a DI container for page objects, services, context objects, drivers, and framework helpers when centralized object management is useful. Avoid creating dependencies using new inside step definitions when DI is available. Keep object lifecycles aligned with scenario execution. Avoid static shared state. Design for loose coupling and parallel execution.

Best practices should be applied pragmatically. Not every small object needs to be injected. Simple value objects and local data structures can still be created normally. DI is most valuable for framework components, shared collaborators, services, page objects, context objects, drivers, and configurable dependencies.

Without DI vs With DI

The difference between a framework without DI and a framework with DI becomes clearer as the project grows. Without DI, objects create their own dependencies, classes are tightly coupled, testing is harder, object creation is duplicated, maintenance is difficult, and scaling becomes painful. With DI, dependencies are provided, coupling is reduced, testing is easier, object management is centralized, maintenance improves, and the framework scales better.

Without DIWith DI
Objects create dependenciesDependencies are provided
Tight couplingLoose coupling
Hard to testEasier to test
Duplicate object creationCentralized object management
Difficult maintenanceEasier maintenance
Less scalableMore scalable

Code Review Checklist

When reviewing Cucumber framework code, check whether step definitions are creating page objects or services manually. Check whether constructor dependencies are reasonable and focused. Check whether WebDriver and scenario context are scenario-scoped. Check whether static mutable state is avoided. Check whether page objects, services, utilities, and validators receive dependencies cleanly.

Also check whether the chosen DI framework is being used consistently. Mixed patterns can make the framework confusing. If some classes use DI and others create dependencies manually without reason, the architecture becomes harder to follow. Consistency is important for long-term maintainability.

Interview-Ready Summary

Dependency Injection is a design pattern where required objects are provided to a class instead of being created by that class. In Cucumber automation, DI helps inject step definitions, page objects, WebDriver, API services, validators, utilities, and scenario context objects. It promotes loose coupling, improves maintainability, simplifies testing, supports object lifecycle management, and reduces duplicated object creation.

Constructor injection is generally preferred because it makes dependencies explicit and supports immutable design. Cucumber frameworks commonly use PicoContainer, Spring, or Guice for dependency injection. Enterprise automation frameworks rely on DI to build scalable, modular, and maintainable test architectures across Selenium UI automation, REST Assured API automation, reporting, logging, and parallel CI execution.

Golden Rules

Prefer constructor injection for required dependencies. Avoid creating page objects, services, drivers, and context objects directly inside step definitions when Dependency Injection is available. Use DI to inject page objects, services, WebDriver, API clients, validators, utilities, and scenario context. Keep object lifecycles scoped appropriately, usually per scenario in Cucumber.

Avoid global static mutable state and design for loose coupling from the beginning. Keep classes focused so they do not require too many dependencies. Choose the DI framework that matches the project rather than adding unnecessary complexity. The practical takeaway is simple: Dependency Injection gives a Cucumber framework controlled object management, clean collaboration between layers, and a structure that can grow without becoming tightly coupled.