Hello Techies,
Before exploring dependency injection in the Cucumber BDD framework, let’s first understand what BDD is all about.
In the world of software development, effective communication between business and technical teams is crucial. It’s often observed that there is still a gap between the needs of the business and the final product developed. This gap arises due to a lack of clear communication about the requirements between the teams.
Behavior-Driven Development (BDD) is a software development process introduced to bridge this gap. It involves continuous collaboration across stakeholders to clearly understand the requirements and how the system should behave from the user's perspective. These requirements are then documented in a simple language that can be easily understood by both technical and non-technical individuals.
“Who will write this document, you might ask?”
The Three Amigos, consisting of the product owner, tester, and developer, will work together to create scenarios (requirements / executable specifications) in plain text. These scenarios should follow basic syntax rules, called Gherkins.
Now, onto the question of how we automate testing the software using Cucumber.
Cucumber was built to support BDD process. It reads these scenarios and validate if the software is working as expected and generates a report indicating success or failure for each scenario.
The following image is to show the flow of test execution in Cucumber.
1. Feature Files:
Each feature file contains one or more scenarios and is written in a Gherkin format (Given-When-Then syntax). Each line in the scenario is commonly referred to as a Gherkin step.
2. Step Definitions:
This is where you will find the corresponding automation code for each gherkin step in the feature file. Validation against the system occurs here.
What is the need for Dependency injection in Cucumber?
When starting a cucumber project, you may not initially require a dependency injection (DI) module. However, as your testing grows, most projects can benefit from implementing a DI module to better organize the code and share state between step definitions.
Let me illustrate with a simple example. We will be testing a web application with multiple web pages.
The test scenarios can be organized in either one feature file or in multiple feature files.
It's recommended to have separate step definition classes for each web page instead of having all the methods in one place.
Additionally, it's beneficial to create page classes for each page, following the Page Object Model design principle, to avoid stale element exceptions.
In this example, I will demonstrate taking two web pages, a Google home page and a Gmail login page, and keeping the scenarios simple for demonstration purposes.
“Below, you will find the Scenarios, Driver manager, and Page object classes.”
Scenarios from 2 feature files:
@home @all
Feature: Validate Google Home page
Scenario: Validate gmail link in google home page
When User enters the application url
Then User should see gmail link in the home page
@login @all
Feature: Validate Gmail Login page
Scenario: Validate navigation from home page to gmail login page
When User enters the application url
And User goes to gmail login page
Then User should be redirected to gmail login page
Driver manager class:
public class DriverManager {
private static WebDriver driver;
public WebDriver initializeDriver() {
if (driver == null) {
driver = new ChromeDriver();
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setPageLoadStrategy(PageLoadStrategy.NORMAL);
chromeOptions.addArguments("start-maximized");
}
return getDriver();
}
public static WebDriver getDriver() {
return driver;
}
}
Page class for Home page:
public class HomePage {
@FindBy(xpath = "//a[text()='Gmail']")
private WebElement gmailLink;
public WebElement getGmailLink() {
return gmailLink;
}
Page class for Gmail page:
public class GmailPage {
@FindBy(xpath = "//a[text()='Sign in']")
private WebElement signInBtn;
public WebElement getSignInBtn() {
return signInBtn;
}
@FindBy(xpath = "//input[@type='email']")
private WebElement emailTxtbox;
public WebElement getEmailTxtbox() {
return emailTxtbox;
}
In the first scenario on the Google Home Page, everything would work well because the corresponding step definition class would only need web elements from the HomePage class. You can simply create an object for the HomePage class to access its properties in the Home Step definition class. Please refer the code below.
Home Page Step Definition class:
public class Home_SD {
@When("User enters the application url")
public void user_enters_the_application_url() {
DriverManager driverManager = new DriverManager();
driverManager.initializeDriver().get("https://www.google.com");
}
@Then("User should see gmail link in the home page")
public void user_should_see_gmail_link_in_the_home_page() {
HomePage homePOM = new HomePage();
Assert.assertTrue(homePOM.getGmailLink().isDisplayed(), "Gmail link is not displayed");
}
}
When it comes to the second scenario, one of the steps in the GmailLogin step definition class requires access to the web elements of both HomePage and GmailPage. In this case, if we go the usual way of manually creating objects for those page classes (shown in the below code), the flow will work fine, and this approach is suitable for simple projects.
Gmail Login Page Step Definition class:
public class GmailLogin_SD {
GmailPage loginPOM;
HomePage homePOM;
@When("User goes to gmail login page")
public void user_goes_to_gmail_login_page() {
homePOM = new HomePage();
homePOM.getGmailLink().click();
loginPOM = new GmailPage();
loginPOM.getSignInBtn().click();
}
@Then("User should be redirected to gmail login page")
public void user_should_be_redirected_to_gmail_login_page() {
Assert.assertEquals(DriverManager.getDriver().getTitle(), "Gmail", "Gmail page title is incorrect");
Assert.assertTrue(loginPOM.getEmailTxtbox().isDisplayed(), "Email link is not displayed");
}
}
However, imagine having multiple step definition classes, each trying to access more than one page object class. Constantly creating objects for the same page class in multiple step definition classes is not ideal. So, how should we handle this situation? The solution is to use Dependency injection design pattern.
"What is Dependency Injection, you might ask?"
Well, let me answer that first. Dependency injection is the process of providing a resource (dependency) that a certain piece of code needs. It is used to make a class independent of its dependencies, thus making them loosely coupled. Dependency injection is also beneficial for improving the reusability of code.
Notably, Cucumber supports Dependency Injection, and the recommended DI module is PicoContainer.
Let’s look at the steps to achieve DI through PicoContainer in the cucumber project.
1. Make sure to add the "cucumber-picocontainer" dependency in the pom.xml file.
<!--https://mvnrepository.com/artifact/io.cucumber/cucumber-picocontainer -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>7.18.1</version>
</dependency>
2. Then, create a container class to declare common classes which can be used across the step definition classes. In this example, the common classes are HomePage, GmailPage and DriverManager.
public class PicoContainerInjector {
private HomePage homePOM;
private GmailPage loginPOM;
private DriverManager driverManager;
public HomePage getHomePOM() {
return (homePOM == null) ? homePOM = new HomePage() : homePOM;
}
public GmailPage getLoginPOM() {
return (loginPOM == null) ? loginPOM = new GmailPage() : loginPOM;
}
public DriverManager getDriverManager() {
return (driverManager == null) ? driverManager = new DriverManager() : driverManager;
}
}
3. The next important step is injecting dependencies, which is achieved through constructor injection. This means passing the dependencies as arguments to the class constructor. This makes them mandatory for object creation and ensures immutability.
Note: Cucumber will create a new instance of each of your step definition classes before each scenario.
Home Page Step Definition class with DI:
public class Home_SD {
PicoContainerInjector picoContainer;
public Home_SD(PicoContainerInjector picoContainer) {
this.picoContainer = picoContainer;
}
@When("User enters the application url")
public void user_enters_the_application_url() {
picoContainer.getDriverManager().initializeDriver().get("https://www.google.com");
}
@Then("User should see gmail link in the home page")
public void user_should_see_gmail_link_in_the_home_page() {
Assert.assertTrue(picoContainer.getHomePOM().getGmailLink().isDisplayed(), "Gmail link is not displayed");
}
}
Gmail Login Page Step Definition class with DI:
public class GmailLogin_SD {
PicoContainerInjector picoContainer;
public GmailLogin_SD(PicoContainerInjector picoContainer) {
this.picoContainer = picoContainer;
}
@When("User goes to gmail login page")
public void user_goes_to_gmail_login_page() {
picoContainer.getHomePOM().getGmailLink().click();
picoContainer.getLoginPOM().getSignInBtn().click();
}
@Then("User should be redirected to gmail login page")
public void user_should_be_redirected_to_gmail_login_page() {
Assert.assertEquals(picoContainer.getDriverManager().initializeDriver().getTitle(), "Gmail",
"Gmail page title is incorrect");
Assert.assertTrue(picoContainer.getLoginPOM().getEmailTxtbox().isDisplayed(), "Email link is not displayed");
}
}
We can clearly see from the above code that, after using PicoContainer, I no longer manually create objects for HomePage, GmailPage, and DriverManager in the step definition classes to access their properties.
In this blog, we have explored PicoContainer, which is a lightweight and versatile dependency injection (DI) module used in a Cucumber project to avoid redundant object creation for dependencies. PicoContainer simply provides the dependencies through the constructor to achieve cleaner, modular, and maintainable automation scripts.
I encourage you to explore more on PicoContainer by implementing it in your project to experience firsthand how it can transform test scripts by managing dependencies seamlessly. Feel free to share your experiences in the comments section. Let's learn together.
Enjoy learning!
Comments