Welcome to the twenty-first part of our Cucumber series for beginners! In the previous post, we explored Integration with Appium, which enables mobile app testing with Cucumber. Now, we’ll dive into Dependency Injection (DI), a technique in Cucumber that simplifies the management of shared objects (e.g., WebDriver, API clients) across step definitions and hooks. This guide will explain what dependency injection is, how to implement it in Cucumber, and provide practical examples to make it easy for beginners and valuable for experienced professionals. Let’s get started!


What is Dependency Injection in Cucumber?

Dependency Injection (DI) is a design pattern that allows objects to receive their dependencies (e.g., shared resources like WebDriver or configuration) from an external source rather than creating them internally. In Cucumber, DI manages shared state (e.g., a WebDriver instance) across step definitions and hooks, reducing code duplication and improving test maintainability.

Cucumber supports DI through libraries like PicoContainer, Spring, or Guice. This guide focuses on PicoContainer, as it’s lightweight and commonly used with Cucumber for Java.

Why Use Dependency Injection?

  • Shared State: Share objects (e.g., WebDriver, API clients) across step definitions without static variables or globals.
  • Modularity: Keep step definitions independent and reusable.
  • Maintainability: Centralize object creation and lifecycle management.
  • Test Isolation: Ensure each scenario has a clean state by managing object scope.
  • Scalability: Support complex test suites with multiple shared resources.

How Dependency Injection Works in Cucumber

Cucumber’s DI works by:

  1. Creating a container (e.g., PicoContainer) to manage object instances.
  2. Injecting dependencies into step definition classes via constructor injection.
  3. Managing the lifecycle of objects (e.g., creating a new WebDriver for each scenario).

Without DI, you might use static variables or pass objects manually, which can lead to tightly coupled code and issues like shared state across scenarios. DI solves this by automatically providing dependencies where needed.


Setting Up Dependency Injection with PicoContainer

Let’s set up a Maven project with Cucumber, Selenium, and PicoContainer to demonstrate DI in a web testing scenario. We’ll share a WebDriver instance across step definitions and hooks.

Step 1: Create a Maven Project

Create a new Maven project in your IDE (e.g., IntelliJ) with the following pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>cucumber-di-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Cucumber Dependencies -->
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>7.18.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>7.18.0</version>
            <scope>test</scope>
        </dependency>
        <!-- PicoContainer for DI -->
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-picocontainer</artifactId>
            <version>7.18.0</version>
            <scope>test</scope>
        </dependency>
        <!-- Selenium Dependency -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>4.25.0</version>
            <scope>test</scope>
        </dependency>
        <!-- JUnit Dependency -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.0</version>
            </plugin>
        </plugins>
    </build>
</project>

Note: The cucumber-picocontainer dependency enables DI with PicoContainer.

Step 2: Download ChromeDriver

Download the ChromeDriver executable compatible with your Chrome browser version from chromedriver.chromium.org. Place it in a directory (e.g., drivers/) in your project.

Step 3: Create a Feature File

Create a file named login.feature in src/test/resources/features:

Feature: User Login
  As a user, I want to log in to the application so that I can access my account.

  Scenario: Successful login with valid credentials
    Given the user is on the login page
    When the user enters "standard_user" and "secret_sauce"
    And the user clicks the login button
    Then the user should be redirected to the dashboard

Note: This example uses saucedemo.com for testing.

Step 4: Create a Shared Context Class

Create a TestContext.java in src/test/java/context to hold shared objects (e.g., WebDriver):

package context;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class TestContext {
    private WebDriver driver;

    public TestContext() {
        System.setProperty("webdriver.chrome.driver", "drivers/chromedriver"); // Update path
        driver = new ChromeDriver();
        driver.manage().window().maximize();
    }

    public WebDriver getDriver() {
        return driver;
    }

    public void quitDriver() {
        if (driver != null) {
            driver.quit();
        }
    }
}

Explanation:

  • TestContext: Manages the WebDriver instance.
  • Initialized by PicoContainer and injected into step definitions and hooks.
  • Includes a cleanup method (quitDriver).

Step 5: Create Step Definitions with DI

Create LoginSteps.java in src/test/java/steps:

package steps;

import context.TestContext;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.And;
import org.junit.Assert;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class LoginSteps {
    private final WebDriver driver;

    public LoginSteps(TestContext context) {
        this.driver = context.getDriver();
    }

    @Given("the user is on the login page")
    public void userIsOnLoginPage() {
        driver.get("https://www.saucedemo.com/");
    }

    @When("the user enters {string} and {string}")
    public void userEntersCredentials(String username, String password) {
        driver.findElement(By.id("user-name")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);
    }

    @And("the user clicks the login button")
    public void userClicksLoginButton() {
        driver.findElement(By.id("login-button")).click();
    }

    @Then("the user should be redirected to the dashboard")
    public void userRedirectedToDashboard() {
        String currentUrl = driver.getCurrentUrl();
        Assert.assertTrue("User not redirected to dashboard", currentUrl.contains("inventory.html"));
    }
}

Explanation:

  • Constructor Injection: The TestContext is injected into LoginSteps by PicoContainer.
  • WebDriver Access: Uses the shared driver from TestContext.

Step 6: Create Hooks with DI

Create Hooks.java in src/test/java/steps:

package steps;

import context.TestContext;
import io.cucumber.java.After;

public class Hooks {
    private final TestContext context;

    public Hooks(TestContext context) {
        this.context = context;
    }

    @After
    public void tearDown() {
        context.quitDriver();
    }
}

Explanation:

  • Hooks: Uses DI to access TestContext and close the WebDriver after each scenario.
  • Ensures proper cleanup without duplicating code.

Step 7: Create a Test Runner

Create TestRunner.java in src/test/java:

package runner;

import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(
    features = "src/test/resources/features",
    glue = {"steps", "context"},
    plugin = {
        "pretty",
        "html:reports/cucumber.html",
        "json:reports/cucumber.json"
    },
    monochrome = true
)
public class TestRunner {
}

Explanation:

  • glue: Includes both steps (for steps and hooks) and context (for TestContext).
  • plugin: Generates HTML and JSON reports.

Step 8: Run the Tests

Ensure ChromeDriver is in the specified path (e.g., drivers/chromedriver). Run the tests using Maven:

mvn test

Output:

Navigating to login page
Entering username: standard_user, password: secret_sauce
Clicking login button
Verifying redirection to dashboard

1 Scenario (1 passed)
4 Steps (4 passed)
0m5.123s

Reports:

  • HTML: Open reports/cucumber.html in a browser.
  • JSON: Check reports/cucumber.json.

Browser Behavior:

  • A Chrome browser opens, navigates to saucedemo.com, logs in, verifies the dashboard, and closes.

Enhancing Dependency Injection

Let’s explore advanced DI techniques to make the integration more robust.

Example 1: Sharing Multiple Objects

Extend TestContext to share additional objects, like a configuration. Update TestContext.java:

package context;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class TestContext {
    private WebDriver driver;
    private String baseUrl;

    public TestContext() {
        System.setProperty("webdriver.chrome.driver", "drivers/chromedriver");
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        baseUrl = "https://www.saucedemo.com/"; // Load from config file in real projects
    }

    public WebDriver getDriver() {
        return driver;
    }

    public String getBaseUrl() {
        return baseUrl;
    }

    public void quitDriver() {
        if (driver != null) {
            driver.quit();
        }
    }
}

Update LoginSteps.java:

@Given("the user is on the login page")
public void userIsOnLoginPage() {
    driver.get(context.getBaseUrl());
}

Explanation:

  • baseUrl: Shared via TestContext for reuse across steps.
  • Simplifies URL management and supports configuration changes.

Example 2: Page Object Model with DI

Use DI with the Page Object Model (POM). Create LoginPage.java in src/test/java/pages:

package pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class LoginPage {
    private WebDriver driver;
    private By usernameField = By.id("user-name");
    private By passwordField = By.id("password");
    private By loginButton = By.id("login-button");

    public LoginPage(WebDriver driver) {
        this.driver = driver;
    }

    public void enterCredentials(String username, String password) {
        driver.findElement(usernameField).sendKeys(username);
        driver.findElement(passwordField).sendKeys(password);
    }

    public void clickLoginButton() {
        driver.findElement(loginButton).click();
    }

    public String getCurrentUrl() {
        return driver.getCurrentUrl();
    }
}

Update LoginSteps.java:

package steps;

import context.TestContext;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.And;
import org.junit.Assert;
import pages.LoginPage;

public class LoginSteps {
    private final LoginPage loginPage;

    public LoginSteps(TestContext context) {
        this.loginPage = new LoginPage(context.getDriver());
    }

    @Given("the user is on the login page")
    public void userIsOnLoginPage() {
        loginPage.getCurrentUrl(); // Navigates to base URL via context
    }

    @When("the user enters {string} and {string}")
    public void userEntersCredentials(String username, String password) {
        loginPage.enterCredentials(username, password);
    }

    @And("the user clicks the login button")
    public void userClicksLoginButton() {
        loginPage.clickLoginButton();
    }

    @Then("the user should be redirected to the dashboard")
    public void userRedirectedToDashboard() {
        String currentUrl = loginPage.getCurrentUrl();
        Assert.assertTrue("User not redirected to dashboard", currentUrl.contains("inventory.html"));
    }
}

Explanation:

  • POM with DI: Injects the WebDriver from TestContext into LoginPage.
  • Improves modularity and maintainability.

Example 3: Scenario-Specific Dependencies

Use a custom object to share scenario-specific data. Create ScenarioContext.java in src/test/java/context:

package context;

public class ScenarioContext {
    private String username;

    public void setUsername(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

Update LoginSteps.java:

public class LoginSteps {
    private final LoginPage loginPage;
    private final ScenarioContext scenarioContext;

    public LoginSteps(TestContext context, ScenarioContext scenarioContext) {
        this.loginPage = new LoginPage(context.getDriver());
        this.scenarioContext = scenarioContext;
    }

    @When("the user enters {string} and {string}")
    public void userEntersCredentials(String username, String password) {
        scenarioContext.setUsername(username);
        loginPage.enterCredentials(username, password);
    }

    @Then("the user should be redirected to the dashboard")
    public void userRedirectedToDashboard() {
        String currentUrl = loginPage.getCurrentUrl();
        System.out.println("Logged in as: " + scenarioContext.getUsername());
        Assert.assertTrue("User not redirected to dashboard", currentUrl.contains("inventory.html"));
    }
}

Explanation:

  • ScenarioContext: Stores scenario-specific data (e.g., username).
  • Injected alongside TestContext for flexible state management.

Best Practices for Dependency Injection

  1. Use a Context Class: Centralize shared objects in a TestContext class.
  2. Keep Dependencies Minimal: Inject only necessary objects to avoid complexity.
  3. Scope Objects to Scenarios: Use DI to ensure fresh instances per scenario, preventing state leakage.
  4. Leverage POM: Combine DI with the Page Object Model for web/mobile testing.
  5. Clean Up Resources: Use @After hooks to close resources like WebDriver.
  6. Document Dependencies: Comment DI classes for team understanding.

Troubleshooting Dependency Injection Issues

  • Missing Dependencies: Ensure cucumber-picocontainer is included in pom.xml.
  • Constructor Errors: Verify step definition constructors match injected classes (e.g., TestContext).
  • Shared State Issues: Use DI to avoid static variables, ensuring scenario isolation.
  • Glue Path Errors: Include context classes in the glue option of TestRunner.
  • Resource Leaks: Confirm @After hooks clean up resources (e.g., driver.quit()).

Tips for Beginners

  • Start with PicoContainer: It’s lightweight and built for Cucumber.
  • Use a Single Context Class: Begin with one TestContext for simplicity.
  • Test DI Locally: Run tests in your IDE to verify dependency injection.
  • Combine with Hooks: Use DI in hooks for setup/teardown tasks.

What’s Next?

You’ve learned how to use Dependency Injection with Cucumber to manage shared objects and improve test modularity. In the next blog post, we’ll explore Best Practices, which provide guidelines for writing effective, maintainable Cucumber tests.

Let me know when you’re ready for the next topic (Best Practices), and I’ll provide a detailed post!

System: * Today's date and time is 05:00 PM IST on Friday, June 06, 2025.