<<<<<<< HEAD
This tutorial is for Cucumber Automation Framework using Maven and Java.
This is an advanced topic and is required basic knowledge and experience in Java, Maven, Cucumber and Gherkin syntax.
This is a step-by-step work through which starts with the basic End-to-to-end test implementation and builds it up into sophisticated and effectively maintainable test automation framework.
Each section is a logical continuation of a previous one. It results in a fully working test automation at the end of each section.
The tutorial must be followed in a consequent order. The following topics will be covered:
- Preparation for framework building: Implementing Cucumber end-to-end test automation
- Introduction to Page Object Design Pattern with Selenium PageFactory
- Implementation of Page Object Manager
- Learning how to read Project Configurations from Property File
- Making File Reader Manager as a Singleton Design Pattern
- Introduction of a WebDriver Manager
- Learning how to share Test Context between steps and Step Definition files
- Implementation of Hooks in Cucumber Framework
- Learning how to use Data tables in Cucumber versions 3 and above
- Learning about Implicit and Explicit Wait in Selenium WebDriver
- Learning how to share data between steps in Cucumber using Scenario Context
- Introduction to Data Driven Testing and how to read test data from Json file
- Introduction to Cucumber Reports
- Learning about Third parties reporting tools and features by implementing Cucumber Extent Report Adapter
- Delete App.java and AppTest.java files if they are present as they are just sample project files created by default by Maven. To delete the files, just right click on the file and select Delete.
- Create a new 'resources' folder under src/test/. As a standard, we keep Cucumber feature files in resources folder. Right click on the src/test/ and create a New Package and specify the name as 'resources'.
- For Eclipse IDE - right click on the root project and select Maven >> Update Project.
(Note: In Eclipse, if any changes are made to Maven POM or to any folder structure, always update project: Maven >> Update Project to reflect the latest changes)
- For IntelliJ IDE - right click on the resource folder and choose Mark directory as >> 'Test resources root'. (Note: In IntelliJ, enable autoupdate - prompt should pop up on the bottom-right corner and we do not need to refresh the project every time)
Search in Maven repository (https://mvnrepository.com) for:
- Selenium: selenium-java; version 3.141.59
- JUnit: junit; version 4.12
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
- cucumber-java, version 4.2.0
- cucumber-junit, version 4.2.0
<properties>
<cucumber.version>4.2.0</cucumber.version>
</properties>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
Note: Cucumber-java and cucumber-junit dependencies required to be of the same version.
Properties tag can be used in pom.xml to create a variable for cucumber related dependencies for easier upgrading
The Compiler Plugin is used to compile the sources of your project. Also note that at present, the default source setting is 1.5 and the default target setting is 1.5, independently of the JDK you run Maven with. If you want to change these defaults, you should set source and target as described in Setting the –source and –target of the Java Compiler. maven-compiler-plugin: version 3.7.0
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
<compilerArgument>-Werror</compilerArgument>
</configuration>
</plugin>
Pom.xml should look like this:
<?xml version="1.0" encoding="UTF-8"?>
<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>Cuke-4-Framework</groupId>
<artifactId>Cuke-4-Framework</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<cucumber.version>4.2.0</cucumber.version>
</properties>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
<compilerArgument>-Werror</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>
- Download Chrome driver from http://chromedriver.chromium.org/downloads
Note: make sure the version of Chrome driver matches the Chrome browser version on you PC!
- Create a New Package and name it 'drivers' by right click on the src and select New >> Package
- Place Chrome driver from your Download folder to project's src/drivers folder
For the purpose of this tutorial, we use the test longer than usual. This is to demonstrate right example of framework components. Cucumber Framework requires to have complex page objects, various configurations and other challenges. This end-to-end scenario could be viewed as a business scenario to automate and it will help us demonstrate Cucumber framework implementation.
- Test to Automate:
User visits Demo Website and searches a 'Dress'.
User selects the first product from the search results and goes to product page.
User successfully adds the product to the cart. User continues to Cart Page from mini cart icon at the top right corner.
Then user moves forward to Checkout page and order details.
User fills in required information, accepts Terms and conditions and proceeds with the order.
User is presented with order confirmation including details on the purchased dress.
- Create a Feature File:
2.1) Create a New Package and name it 'features' by right click on the src/test/resources and select New >> Package. (Note: It is always recommended to put all the feature files in the resources folder).
2.2) Create a Feature file and name it 'End2End_Test.feature' by right click on the above created package and select New >> File.
Note: all feature files must have .feature extension
- Add the test steps to the feature file as follows:
Feature: Automated End2End Tests
Description: The purpose of this feature is to test End 2 End integration.
Scenario: Customer place an order by purchasing an item from search
Given I am on Home Page
When I search for product in dress category
And I choose to buy the first item
And I move to checkout from mini cart
And I enter my personal details
And I place the order
Then Order details are successfully verified
- Create a New Package and name it 'runners' by right click on the src/test/java and select New >> Package.
- Create a New Java Class file and name it 'TestRunner' by right click on the above created package and select New >> Class.
Note: It is important to have a key word 'Test' as a part of a runner class name so test(s) can be run from the command line using Maven
To get the steps automatically generated, we need to execute TestRunner class. Right click on the TestRunner file and select:
Eclipse: Run As >> JUnit Test
IntelliJ: Run TestRunner
You would get the below result in the IDE Console:
Undefined scenarios:
src/test/resources/features/End2End_Test.feature:4 # Customer place an order by purchasing an item from search
1 Scenarios (1 undefined)
7 Steps (7 undefined)
0m0.399s
You can implement missing steps with the snippets below:
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
// Write code here that turns the phrase above into concrete actions
throw new cucumber.api.PendingException();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() {
// Write code here that turns the phrase above into concrete actions
throw new cucumber.api.PendingException();
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() {
// Write code here that turns the phrase above into concrete actions
throw new cucumber.api.PendingException();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() {
// Write code here that turns the phrase above into concrete actions
throw new cucumber.api.PendingException();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() {
// Write code here that turns the phrase above into concrete actions
throw new cucumber.api.PendingException();
}
@When("I place the order")
public void i_place_the_order() {
// Write code here that turns the phrase above into concrete actions
throw new cucumber.api.PendingException();
}
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
// Write code here that turns the phrase above into concrete actions
throw new cucumber.api.PendingException();
}
Process finished with exit code 0
- Create a New Package and name it 'stepDefinitions' by right click on the src/test/java and select New >> Package.
- Create a New Java Class and name it 'Steps' by right click on the above created package and select New >> Class.
- Now copy all the steps created by IDE to this Steps file and start filling up these steps with Selenium Code. Steps test file will look like this:
package stepDefinitions;
import java.util.List;
import java.util.concurrent.TimeUnit;
import cucumber.api.java.en.Then;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import org.openqa.selenium.support.ui.Select;
public class Steps {
WebDriver driver;
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
//Get Chrome driver
System.setProperty("webdriver.chrome.driver","src/drivers/chromedriver");
driver = new ChromeDriver();
//Open Chrome driver
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
//Navigate to Home page
driver.get("http://www.shop.demoqa.com");
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() throws InterruptedException {
//Explicit wait is added to wait for elements to load on a page
Thread.sleep(2000);
//On Home page, search for "dress" product category
WebElement btn_search = driver.findElement(By.cssSelector(".noo-search"));
btn_search.click();
Thread.sleep(2000);
WebElement input_search = driver.findElement(By.cssSelector("input.form-control"));
input_search.sendKeys("dress");
input_search.sendKeys(Keys.RETURN);
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() throws InterruptedException {
Thread.sleep(2000);
//On Product page, get all items displayed on the page from the search result
List<WebElement> items = driver.findElements(By.cssSelector(".noo-product-inner"));
//Click on the first item
items.get(0).click();
//Select colour and size
WebElement select_colour = driver.findElement(By.id("pa_color"));
Select colour = new Select(select_colour);
colour.selectByIndex(1);
WebElement select_size = driver.findElement(By.id("pa_size"));
Select size = new Select(select_size);
size.selectByIndex(1);
//Add item to cart
WebElement addToCart = driver.findElement(By.cssSelector("button.single_add_to_cart_button"));
addToCart.click();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() throws InterruptedException{
Thread.sleep(2000);
//On Cart page, click on Cart element
WebElement cart = driver.findElement(By.cssSelector(".cart-button"));
cart.click();
Thread.sleep(2000);
//And click on checkout button
WebElement continueToCheckout = driver.findElement(By.cssSelector(".checkout-button.alt"));
continueToCheckout.click();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() throws InterruptedException {
Thread.sleep(2000);
//On Checkout page, fill in customer details
WebElement firstName = driver.findElement(By.id("billing_first_name"));
firstName.sendKeys("TestAutomation");
WebElement lastName = driver.findElement(By.id("billing_last_name"));
lastName.sendKeys("Opencast");
WebElement select_Country = driver.findElement(By.id("billing_country"));
Select country = new Select(select_Country);
country.selectByVisibleText("United Kingdom (UK)");
WebElement address = driver.findElement(By.id("billing_address_1"));
address.sendKeys("Hoults Yard, Walker Road");
WebElement city = driver.findElement(By.id("billing_city"));
city.sendKeys("Newcastle upon Tyne");
WebElement postcode = driver.findElement(By.id("billing_postcode"));
postcode.sendKeys("NE6 3PE");
//Page gets refreshed after the postcode is entered, so we introduce an extra wait
Thread.sleep(2000);
WebElement phone = driver.findElement(By.id("billing_phone"));
phone.sendKeys("07438862327");
WebElement emailAddress = driver.findElement(By.id("billing_email"));
emailAddress.sendKeys("test@test.com");
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
Thread.sleep(2000);
//On Checkout page, click on T&Cs and submit the order
WebElement chkbx_AcceptTermsAndCondition = driver.findElement(By.cssSelector(".woocommerce-form__input-checkbox"));
chkbx_AcceptTermsAndCondition.click();
WebElement btn_PlaceOrder = driver.findElement(By.id("place_order"));
btn_PlaceOrder.submit();
Thread.sleep(2000);
driver.manage().deleteAllCookies();
driver.close();
driver.quit();
}
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
//User is automatically re-directed to the Order confirmation page. Validation step will be implemented later on this course
System.out.println("Not implemented");
//Closing the browser
}
}
- Update TestRunner class
We also need to make sure that the TestRunner would able to find the steps files.
To achieve that we need to mention the path of the StepDefinition package in @CucumberOptions.
Note: By default Junit/Cucumber finds the test code in the src/test/java folder, this is why we just need to specify the package name for the cucumber glue.
Updated TestRunner class should look like this:
package runners;
import org.junit.runner.RunWith;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
glue= {"stepDefinitions"}
)
public class TestRunner {
}
- Run Cucumber test
- Run as JUnit Right Click on TestRunner class and Click Run Test. Cucumber will run the script and the result will be shown in the left hand side project explorer window in JUnit tab.
- Run from Command Prompt From IDE Terminal (which sets the location automatically to the root of our project) run the following command:
mvn clean compile test
Out end-to-end test should be executed successfully
This section is about Page Object Model Framework which is also known as Page Object Design Pattern or Page Object. The main advantage of Page Object Model is that if the UI or any HTML object changes for any page, the test does not need any fix. Only the code within the page objects will be affected but it does not have any impact on the test code.
Till now we have just one test and one step definition file. In real live projects we will be dealing with hundreds of tests which all have multiple Step Definition files. The whole project code will become unmanageable and unmaintainable. To better manage the code and to improve the re-usability, Page Object Design pattern suggests us to divide an application to sub pages / sections. So far we were writing a code with no actual structure, focusing only on elements and sending commands to Selenium driver.
The Page Object Pattern technique provides a solution for working with multiple web pages. It will help prevent unwanted code duplication and enable an effective solution for a code maintenance. In general, every page of the application involved in our end-to-end testing will be represented by a unique class of its own. Such class will include both page element inspection and associated actions performed by Selenium on the corresponding page.
In order to implement the Page Object Model we will be using Selenium PageFactory
Selenium PageFactory is an inbuilt Page Object Model concept for Selenium WebDriver and it is very optimized. PageFactory is used to Initialise Elements of a Page class without having to use ‘FindElement()‘ or ‘FindElements()‘ methods. Annotations can be used to supply descriptive names of target objects to improve code readability.
@FindBy Annotation:
As the name suggest, it helps to find the elements in the page using By strategy. @FindBy can accept TagName, PartialLinkText, Name, LinkText, Id, Css, ClassName and XPath as an attribute.
@FindBy(id = “idname“)]
public WebElement element;
The above code will create a PageObject and name it 'element' by finding it using its 'id' locator.
InitElements:
This Instantiate an Instance of the given class. This method will attempt to instantiate the class given to it, preferably using a constructor which takes a WebDriver instance as its only argument An exception will be thrown if the class cannot be instantiated.
PageFactory.initElements(WebDriver, PageObject.Class);
Parameters:
-
WebDriver – The driver that will be used to look up the elements
-
PageObject – A class which will be initialised
Returns: An instantiated instance of the class with WebElement and List fields proxies
PageFactory NameSpace:
In order to use PageFactory, org.openqa.selenium.support.PageFactory needs to be imported to the associated Class.
The flow of our test spreads across the following pages:
- Home Page
- Product Listing Page
- Cart Page
- Checkout Page
- Confirmation Page
Therefore, we will be creating corresponding Java Classes in our Project like so:
- HomePage.java
- ProductListingPage.java
- CartPage.java
- CheckoutPage.java -ConfirmationPage.java
-
Create a New Package file and name it 'pageObjects', by right click on the src/main/java and select New >> Package.
-
Create five New Class files and name them as was mentioned above
-
Initiate the Page Object for each Class using Constructor. Example of CheckoutPage initiation:
public CheckoutPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
- Move WebElement locators associated with a particular page to a corresponding Page Object Class and replace 'findElement(s) (By by)' method with @FindBy annotation.
- Wrap Selenium actions performed on each page into re-usable methods and again, place them into corresponding Page Object Class.
Note: ConfirmationPage.java is the only Class which we leave blank as we have no implementation of @Then Step yet
Newly created Page Object Classes should looks like this:
package pageObjects;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class HomePage {
WebDriver driver;
public HomePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
@FindBy(css=".noo-search")
public WebElement btn_Search;
@FindBy(css=".form-control")
public WebElement input_Search;
public void navigateTo_HomePage() {
driver.get("http://www.shop.demoqa.com");
}
public void perform_Search(String search) {
btn_Search.click();
input_Search.sendKeys(search);
input_Search.sendKeys(Keys.RETURN);
}
}
package pageObjects;
import java.util.List;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.Select;
public class ProductListingPage {
public ProductListingPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
@FindBy(css = "button.single_add_to_cart_button")
public WebElement btn_AddToCart;
@FindAll(@FindBy(css = ".noo-product-inner"))
public List<WebElement> prd_List;
@FindBy(id="pa_color")
public WebElement selectColour;
@FindBy(id="pa_size")
public WebElement selectSize;
public void select_Product(int productNumber) {
prd_List.get(productNumber).click();
}
public void makeSelection(int index) {
Select colour = new Select(selectColour);
colour.selectByIndex(index);
Select size = new Select(selectSize);
size.selectByIndex(index);
}
public void clickOn_AddToCart() {
btn_AddToCart.click();
}
}
package pageObjects;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class CartPage {
public CartPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
@FindBy(css = ".cart-button")
public WebElement btn_Cart;
@FindBy(css = ".checkout-button.alt")
public WebElement btn_ContinueToCheckout;
public void clickOn_Cart() {
btn_Cart.click();
}
public void clickOn_ContinueToCheckout(){
btn_ContinueToCheckout.click();
}
}
package pageObjects;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.Select;
public class CheckoutPage {
public CheckoutPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
@FindBy(id = "billing_first_name")
public WebElement txtbx_FirstName;
@FindBy(id = "billing_last_name")
public WebElement txtbx_LastName;
@FindBy(id = "billing_email")
public WebElement txtbx_Email;
@FindBy(id = "billing_phone")
public WebElement txtbx_Phone;
@FindBy(id = "billing_country")
public WebElement select_Country;
@FindBy(id = "billing_city")
public WebElement txtbx_City;
@FindBy(id = "billing_address_1")
public WebElement txtbx_Address;
@FindBy(css = "#billing_postcode")
public WebElement txtbx_PostCode;
@FindBy(css = ".woocommerce-form__input-checkbox")
public WebElement chkbx_AcceptTermsAndCondition;
@FindBy(id = "place_order")
public WebElement btn_PlaceOrder;
public void enter_Name(String name) {
txtbx_FirstName.sendKeys(name);
}
public void enter_LastName(String lastName) {
txtbx_LastName.sendKeys(lastName);
}
public void enter_Email(String email) {
txtbx_Email.sendKeys(email);
}
public void enter_Phone(String phone) {
txtbx_Phone.sendKeys(phone);
}
public void enter_City(String city) {
txtbx_City.sendKeys(city);
}
public void enter_Address(String address) {
txtbx_Address.sendKeys(address);
}
public void enter_PostCode(String postCode) {
txtbx_PostCode.sendKeys(postCode);
}
public void select_Country(String countryName) {
Select country = new Select(select_Country);
country.selectByVisibleText(countryName);
}
public void check_TermsAndCondition() {
chkbx_AcceptTermsAndCondition.click();
}
public void clickOn_PlaceOrder() {
btn_PlaceOrder.submit();
}
public void fill_PersonalDetails() throws InterruptedException {
enter_Name("TestAutomation");
enter_LastName("Opencast");
select_Country("United Kingdom (UK)");
enter_Address("Hoults Yard, Walker Road");
enter_City("Newcastle upon Tyne");
enter_PostCode("NE6 3PE");
Thread.sleep(2000);
enter_Phone("07438862327");
enter_Email("test@test.com");
}
}
package pageObjects;
public class ConfirmationPage {
}
It should look like this:
package stepDefinitions;
import java.util.concurrent.TimeUnit;
import cucumber.api.java.en.Then;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
public class Steps {
WebDriver driver;
HomePage homePage;
ProductListingPage productListingPage;
CartPage cartPage;
CheckoutPage checkoutPage;
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
System.setProperty("webdriver.chrome.driver","src/drivers/chromedriver");
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
homePage = new HomePage(driver);
homePage.navigateTo_HomePage();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() throws InterruptedException {
Thread.sleep(1000);
homePage.perform_Search("dress");
Thread.sleep(1000);
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() throws InterruptedException {
productListingPage = new ProductListingPage(driver);
Thread.sleep(1000);
productListingPage.select_Product(0);
productListingPage.makeSelection(1);
productListingPage.clickOn_AddToCart();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() throws InterruptedException{
cartPage = new CartPage(driver);
Thread.sleep(1000);
cartPage.clickOn_Cart();
cartPage.clickOn_ContinueToCheckout();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() throws InterruptedException {
checkoutPage = new CheckoutPage(driver);
Thread.sleep(1000);
checkoutPage.fill_PersonalDetails();
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
checkoutPage = new CheckoutPage(driver);
Thread.sleep(1000);
checkoutPage.check_TermsAndCondition();
checkoutPage.clickOn_PlaceOrder();
driver.manage().deleteAllCookies();
driver.close();
driver.quit();
}
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
System.out.println("Not implemented");
}
}
Run TestRunner and the test should be executed successfully
In this section we will design a Page Object Manager. In previous section we have created objects for our pages like so:
HomePage homePage = new HomePage(driver);
ProductListingPage productListingPage = new ProductListingPage(driver);
cartPage = new CartPage(driver);
checkoutPage = new CheckoutPage(driver);
But there is a problem here. So far we have just one single Cucumber Step Definition file. But in the case of multiple step definition files, we will be creating object of Pages again and again. This is against the coding principle.
To avoid this situation, we can create a Page Object Manager.
The purpose of the Page Object Manger is to create a page object and also to make sure that the object is only created once and it can be used across all step definition files.
-
Create a New Package in src/main/java and name it 'managers'.
-
Create a New Class inside 'managers' package and name it 'PageObjectManager'.
-
Add the following code to the class
package managers;
import org.openqa.selenium.WebDriver;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.ConfirmationPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
public class PageObjectManager {
private WebDriver driver;
private ProductListingPage productListingPage;
private CartPage cartPage;
private HomePage homePage;
private CheckoutPage checkoutPage;
private ConfirmationPage confirmationPage;
// Constructor
public PageObjectManager(WebDriver driver) {
this.driver = driver;
}
//Creating an Object of Page Class only if the object is null,
//Supplying the already created object if it is not null:
public HomePage getHomePage(){
return (homePage == null) ? homePage = new HomePage(driver) : homePage;
}
public ProductListingPage getProductListingPage() {
return (productListingPage == null) ? productListingPage = new ProductListingPage(driver) : productListingPage;
}
public CartPage getCartPage() {
return (cartPage == null) ? cartPage = new CartPage(driver) : cartPage;
}
public CheckoutPage getCheckoutPage() {
return (checkoutPage == null) ? checkoutPage = new CheckoutPage(driver) : checkoutPage;
}
}
Constructor:
public PageObjectManager(WebDriver driver) {
this.driver = driver;
}
This constructor is asking for parameter of type WebDriver: to create an object of the Pages, this class requires a driver.
Therefore to create an object of PageObjectManager class, driver also needs to be provided:
PageObjectManager pageObjectManager = new PageObjectManager(driver);
Page Object Creation Method:
public HomePage getHomePage() {
return (homePage == null) ? new HomePage(driver) : homePage;
}
This method has two responsibilities:
- To create an Object of Page Class only if the object is null.
- To supply the already created object if it is not null
Implementation of PageObjectManager requires change in our step definition file as well. Now the duty of the creation of all the pages assigned to only one class which is Page Object Manager.
package stepDefinitions;
import java.util.concurrent.TimeUnit;
import cucumber.api.java.en.Then;
import managers.PageObjectManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
public class Steps {
WebDriver driver;
HomePage homePage;
ProductListingPage productListingPage;
CartPage cartPage;
CheckoutPage checkoutPage;
PageObjectManager pageObjectManager;
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
System.setProperty("webdriver.chrome.driver","src/drivers/chromedriver");
driver = new ChromeDriver();
pageObjectManager = new PageObjectManager(driver);
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
homePage = pageObjectManager.getHomePage();
homePage.navigateTo_HomePage();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() throws InterruptedException {
homePage = new HomePage(driver);
Thread.sleep(1000);
homePage.perform_Search("dress");
Thread.sleep(1000);
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() throws InterruptedException {
Thread.sleep(1000);
productListingPage = pageObjectManager.getProductListingPage();
productListingPage.select_Product(0);
productListingPage.makeSelection(1);
productListingPage.clickOn_AddToCart();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() throws InterruptedException{
Thread.sleep(1000);
cartPage = pageObjectManager.getCartPage();
cartPage.clickOn_Cart();
cartPage.clickOn_ContinueToCheckout();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() throws InterruptedException {
Thread.sleep(1000);
checkoutPage = pageObjectManager.getCheckoutPage();
checkoutPage.fill_PersonalDetails();
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.check_TermsAndCondition();
checkoutPage.clickOn_PlaceOrder();
driver.manage().deleteAllCookies();
driver.close();
driver.quit();
}
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
System.out.println("Not implemented");
}
}
Run TestRunner and the test should be executed successfully
So far in our project we have been storing hard coded values inside the project code. It is against the coding principles to do so as it makes our test less manageable and maintainable. Therefore with the help of 'properties' file we will be focusing on eliminating these hard coded values.
.properties files are mainly used in Java programs to maintain project configuration data, database config, project settings, etc. Each parameter in .properties file is stored as a pair of strings, in key-value pair format. You can easily read properties from this file using object of type Properties. This is a utility provided by Java itself:
java.util.Properties;
If any information is changed from the properties file, you don’t need to recompile the java class. In other words, the advantage of using properties file is we can configure things which are prone to change over a period of time without a need of changing test code.
- Create a New Folder and name it 'configs', by right click on the root Project and select New >> Folder.
- Create a New File by right click on the above created folder and name it 'Configuration.properties'
- Write Hard Coded Values in the Configuration.properties File.
So far there are three hard coded values we will move to our Configuration.properties:
driverPath=src/drivers/chromedriver
url=http://shop.demoqa.com
implicitWait=5
-
Create a New Package under src/main/java/ and name it 'dataProviders'. We will keep all the data reader files here in this package.
-
Create a New Class file inside 'dataProviders' package and name it 'ConfigFileReader'.
-
Add the following code to ConfigFileReader
package dataProviders;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class ConfigFileReader {
private Properties properties;
private final String propertyFilePath= "configs/Configuration.properties";
public ConfigFileReader(){
BufferedReader reader;
try {
reader = new BufferedReader(new FileReader(propertyFilePath));
//Configuration properties can be easily read from .properties file using object of type Properties provided by java.utils
properties = new Properties();
try {
properties.load(reader);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
throw new RuntimeException("Configuration.properties not found at " + propertyFilePath);
}
}
public String getDriverPath(){
String driverPath = properties.getProperty("driverPath");
if(driverPath!= null) return driverPath;
else throw new RuntimeException("driverPath not specified in the Configuration.properties file.");
}
public long getImplicitWait() {
String implicitWait = properties.getProperty("implicitWait");
if(implicitWait != null) return Long.parseLong(implicitWait);
else throw new RuntimeException("implicitlyWait not specified in the Configuration.properties file.");
}
public String getApplicationUrl() {
String url = properties.getProperty("url");
if(url != null) return url;
else throw new RuntimeException("url not specified in the Configuration.properties file.");
}
}
BufferedReader reader = new BufferedReader(new FileReader(propertyFilePath));
Properties properties = new Properties();
properties.load(reader);
propertyFilePath : This is just a String variable which holds the information of the config file path.
new FileReader(propertyFilePath) : Creates a new FileReader, given the name of the file to read from.
new BufferedReader(new FileReader(propertyFilePath)) : Reads text from a character-input stream, buffering characters so as to provide for the efficient reading of characters, arrays, and lines.
new Properties() : The Properties class represents a persistent set of properties. The Properties can be saved to a stream or loaded from a stream. Each key and its corresponding value in the property list is a string.
properties.load(reader) : Reads a property list (key and value) from the input character stream in a simple line-oriented format.
public String getDriverPath(){
String driverPath = properties.getProperty("driverPath");
if(driverPath!= null) return driverPath;
else throw new RuntimeException("driverPath not specified in the Configuration.properties file.");
}
properties.getProperty(“driverPath”) : Properties object gives us a getProperty() method which takes the Key of the property as a parameter and return the Value of the matching key from the .properties file. If the properties file does not have the specified key, it returns null. This is why we have put the null check and in case of null we like to throw an exception with the stack trace information and stop the test.
To use the ConfigFileReader object in the test, we need to fist create an object of the class.
ConfigFileReader configFileReader= new ConfigFileReader();
Then we can replace the below statement
System.setProperty(“webdriver.chrome.driver”,“src/drivers/chromedriver”);
with
System.setProperty(“webdriver.chrome.driver”, configFileReader.getDriverPath());
Complete Steps file will look like this now:
package stepDefinitions;
import cucumber.api.java.en.Then;
import dataProviders.ConfigFileReader;
import managers.PageObjectManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
import java.util.concurrent.TimeUnit;
public class Steps {
WebDriver driver;
HomePage homePage;
ProductListingPage productListingPage;
CartPage cartPage;
CheckoutPage checkoutPage;
PageObjectManager pageObjectManager;
ConfigFileReader configFileReader;
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
configFileReader= new ConfigFileReader();
System.setProperty("webdriver.chrome.driver", configFileReader.getDriverPath());
driver = new ChromeDriver();
pageObjectManager = new PageObjectManager(driver);
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(configFileReader.getImplicitWait(), TimeUnit.SECONDS);
homePage = pageObjectManager.getHomePage();
homePage.navigateTo_HomePage();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() throws InterruptedException {
homePage = new HomePage(driver);
Thread.sleep(1000);
homePage.perform_Search("dress");
Thread.sleep(1000);
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() throws InterruptedException {
Thread.sleep(1000);
productListingPage = pageObjectManager.getProductListingPage();
productListingPage.select_Product(0);
productListingPage.makeSelection(1);
productListingPage.clickOn_AddToCart();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() throws InterruptedException{
Thread.sleep(1000);
cartPage = pageObjectManager.getCartPage();
cartPage.clickOn_Cart();
cartPage.clickOn_ContinueToCheckout();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() throws InterruptedException {
Thread.sleep(1000);
checkoutPage = pageObjectManager.getCheckoutPage();
checkoutPage.fill_PersonalDetails();
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.check_TermsAndCondition();
checkoutPage.clickOn_PlaceOrder();
driver.manage().deleteAllCookies();
driver.close();
driver.quit();
}
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
System.out.println("Not implemented");
}
}
And our Home Page object class file will look like this:
package pageObjects;
import dataProviders.ConfigFileReader;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class HomePage {
WebDriver driver;
ConfigFileReader configFileReader;
public HomePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
configFileReader= new ConfigFileReader();
}
@FindBy(css=".noo-search")
public WebElement btn_Search;
@FindBy(css=".form-control")
public WebElement input_Search;
public void navigateTo_HomePage() {
driver.get(configFileReader.getApplicationUrl());
}
public void perform_Search(String search) {
btn_Search.click();
input_Search.sendKeys(search);
input_Search.sendKeys(Keys.RETURN);
}
}
Note: Generally, it is bad practice to create object of ConfigFileReader class in every class which requires it. We have created the object of the ConfigFileReader Class in Steps file and another object of this Class again in the HomePage class.
We will cover how to overcome this issue in the next section.
Run TestRunner and the test should be executed successfully
In the previous section, we run into a problem of having multiple instances of ConfigFileReader Class in our project. In this section we will use File Reader Manager as Singleton Design Pattern to eliminate the issue. Singleton Design Pattern helps in achieving that we only have one instance of a class which can be accessed globally.
The Singleton’s purpose is to control object creation, limiting the number of objects to only one. Since there is only one Singleton instance, any instance fields of a Singleton will occur only once per class, just like static fields.
To implement Singleton pattern, we have to implement the following concept:
- Private constructor to restrict instantiation of the class from other classes.
- Private static variable of the same class that is the only instance of the class.
- Public static method that returns the instance of the class, this is the global access point to get the instance of the singleton class.
- Create a New Class in src/main/java inside 'managers' package and name it 'FileReaderManager'.
- Add the following code so File Reader Manager looks like this:
package managers;
import dataProviders.ConfigFileReader;
public class FileReaderManager {
private static FileReaderManager fileReaderManager = new FileReaderManager();
private static ConfigFileReader configFileReader;
private FileReaderManager() {
}
public static FileReaderManager getInstance( ) {
return fileReaderManager;
}
public ConfigFileReader getConfigReader() {
return (configFileReader == null) ? new ConfigFileReader() : configFileReader;
}
}
FileReaderManager class maintains a static reference to its own instance and returns that reference from the static getInstance() method.
FileReaderManager implements a private constructor so clients cannot instantiate FileReaderManager instances.
getConfigReader() method returns the instance of the ConfigFileReader but this method is not static. So that client has to use the getInstance() method to access other public methods of the FileReaderManager like
FileReaderManager.getInstance().getConfigReader()
package stepDefinitions;
import cucumber.api.java.en.Then;
import managers.PageObjectManager;
import managers.FileReaderManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
import java.util.concurrent.TimeUnit;
public class Steps {
WebDriver driver;
HomePage homePage;
ProductListingPage productListingPage;
CartPage cartPage;
CheckoutPage checkoutPage;
PageObjectManager pageObjectManager;
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
System.setProperty("webdriver.chrome.driver", FileReaderManager.getInstance().getConfigReader().getDriverPath());
driver = new ChromeDriver();
pageObjectManager = new PageObjectManager(driver);
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(FileReaderManager.getInstance().getConfigReader().getImplicitWait(), TimeUnit.SECONDS);
homePage = pageObjectManager.getHomePage();
homePage.navigateTo_HomePage();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() throws InterruptedException {
homePage = new HomePage(driver);
Thread.sleep(1000);
homePage.perform_Search("dress");
Thread.sleep(1000);
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() throws InterruptedException {
Thread.sleep(1000);
productListingPage = pageObjectManager.getProductListingPage();
productListingPage.select_Product(0);
productListingPage.makeSelection(1);
productListingPage.clickOn_AddToCart();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() throws InterruptedException{
Thread.sleep(1000);
cartPage = pageObjectManager.getCartPage();
cartPage.clickOn_Cart();
cartPage.clickOn_ContinueToCheckout();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() throws InterruptedException {
Thread.sleep(1000);
checkoutPage = pageObjectManager.getCheckoutPage();
checkoutPage.fill_PersonalDetails();
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.check_TermsAndCondition();
checkoutPage.clickOn_PlaceOrder();
driver.manage().deleteAllCookies();
driver.close();
driver.quit();
}
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
System.out.println("Not implemented");
}
}
package pageObjects;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import managers.FileReaderManager;
public class HomePage {
WebDriver driver;
public HomePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
@FindBy(css=".noo-search")
public WebElement btn_Search;
@FindBy(css=".form-control")
public WebElement input_Search;
public void navigateTo_HomePage() {
driver.get(FileReaderManager.getInstance().getConfigReader().getApplicationUrl());
}
public void perform_Search(String search) {
btn_Search.click();
input_Search.sendKeys(search);
input_Search.sendKeys(Keys.RETURN);
}
}
Run TestRunner and the test should be executed successfully
Why do we need WebDriver Manager? Till now we have been creating driver within the Step definition file and explicitly tell our script to start Chrome Driver. Consequences of that are the following:
- Test script handles the logic of creating WebDriver which is not the best coding practice as code inside test steps should only be responsible for test execution
- Switching between browsers (e.g. Chrome to Firefox) means changing code for every test containing initialisation of a driver
The only responsibility of the WebDriver Manager is to provide the WebDriver, when we ask for it. To achieve this we will do the following:
- Specify new WebDriver Properties in the Configuration File
- Create Enums for DriverType and EnvironmentType
- Write new Methods to read the above properties
- Design a WebDriver Manager
- Modify the Steps file to use the new WebDriver Manager in the script
Configuration file should look like this:
environment=local
browser=chrome
windowMaximize=true
driverPath=src/drivers/chromedriver
url=http://shop.demoqa.com
implicitWait=5
It is always considered a good practice to create enums for all the hard codded values in the project.
- Create a New Package in src/main/java and name it 'enums'. We will be keeping all the project enums in this package.
- Create a New Enum and name it 'DriverType' by right click on the above created Package and select New >> Enum.
- Add one value for Chrome and the other value for FireFox. There can be other browsers as well but lets just take Chrome and Firefox for now.
DriverType file should look like this:
package enums;
public enum DriverType {
FIREFOX,
CHROME
}
- Create another Enum class 'EnvironmentType' and add Local and Remote environmental variables to it.
package enums;
public enum EnvironmentType {
LOCAL,
REMOTE,
}
In ConfigFileReader create getBrowser(), getEnvironment() and getBrowserWindowSize() methods to read newly added properties ConfigFileReader should look like this:
package dataProviders;
import enums.DriverType;
import enums.EnvironmentType;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class ConfigFileReader {
private Properties properties;
private final String propertyFilePath= "configs/Configuration.properties";
public ConfigFileReader(){
BufferedReader reader;
try {
reader = new BufferedReader(new FileReader(propertyFilePath));
//Configuration properties can be easily read from .properties file using object of type Properties provided by java.utils
properties = new Properties();
try {
properties.load(reader);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
throw new RuntimeException("Configuration.properties not found at " + propertyFilePath);
}
}
public String getDriverPath(){
String driverPath = properties.getProperty("driverPath");
if(driverPath!= null) return driverPath;
else throw new RuntimeException("driverPath not specified in the Configuration.properties file.");
}
public long getImplicitWait() {
String implicitWait = properties.getProperty("implicitWait");
if(implicitWait != null) return Long.parseLong(implicitWait);
else throw new RuntimeException("implicitlyWait not specified in the Configuration.properties file.");
}
public String getApplicationUrl() {
String url = properties.getProperty("url");
if(url != null) return url;
else throw new RuntimeException("url not specified in the Configuration.properties file.");
}
public DriverType getBrowser() {
String browserName = properties.getProperty("browser");
if(browserName == null || browserName.equals("chrome")) return DriverType.CHROME;
else if(browserName.equalsIgnoreCase("firefox")) return DriverType.FIREFOX;
else throw new RuntimeException("Browser Name Key value in Configuration.properties is not matched : " + browserName);
}
public EnvironmentType getEnvironment() {
String environmentName = properties.getProperty("environment");
if(environmentName == null || environmentName.equalsIgnoreCase("local")) return EnvironmentType.LOCAL;
else if(environmentName.equals("remote")) return EnvironmentType.REMOTE;
else throw new RuntimeException("Environment Type Key value in Configuration.properties is not matched : " + environmentName);
}
public Boolean getBrowserWindowSize() {
String windowSize = properties.getProperty("windowMaximize");
if(windowSize != null) return Boolean.valueOf(windowSize);
return true;
}
}
getBrowserWindowSize() : Retrieve the property using getProperty method of Properties class. Null check is performed and in case of null by default value is returned as true. In case of not null, String value is parsed to Boolean.
getEnvironment() : EnvironmentType.Local is returned in case of Null and if the value is equal to Local. It means that in case of missing environment property, execution will be carried on local machine.
getBrowser() : Default value is returned as DriverType.Chrome in case of Null. Exception is thrown if the value does not match with anything.
Now it is the time to design the WebDriver Manager. The only thing which we need to keep in mind is that the manager would expose only two method for now which are getDriver() and closeDriver().
GetDriver() method will decide if the driver is already created or needs to be created. GetDriver() method further call the method
createDriver(), which will decide that the remote or local driver is needed for execution.
Accordingly, CreateDriver() method would make a call let’s say to createLocalDriver().
CreateLocalDriver() method will further decide which type of driver needs to be created.
closeDriver() method is responsible for closing the browser and will be called after execution of all tests / test steps is completed
- Create a new file in src/main/java/managers and call it 'WebDriverManager'
- Add the following code to it:
package managers;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import enums.DriverType;
import enums.EnvironmentType;
public class WebDriverManager {
private WebDriver driver;
private static DriverType driverType;
private static EnvironmentType environmentType;
private static final String CHROME_DRIVER_PROPERTY = "webdriver.chrome.driver";
public WebDriverManager() {
driverType = FileReaderManager.getInstance().getConfigReader().getBrowser();
environmentType = FileReaderManager.getInstance().getConfigReader().getEnvironment();
}
public WebDriver getDriver() {
if(driver == null) driver = createDriver();
return driver;
}
private WebDriver createDriver() {
switch (environmentType) {
case LOCAL : driver = createLocalDriver();
break;
case REMOTE : driver = createRemoteDriver();
break;
}
return driver;
}
private WebDriver createRemoteDriver() {
throw new RuntimeException("RemoteWebDriver is not yet implemented");
}
private WebDriver createLocalDriver() {
switch (driverType) {
case FIREFOX : driver = new FirefoxDriver();
break;
case CHROME :
System.setProperty(CHROME_DRIVER_PROPERTY, FileReaderManager.getInstance().getConfigReader().getDriverPath());
driver = new ChromeDriver();
break;
}
if(FileReaderManager.getInstance().getConfigReader().getBrowserWindowSize()) driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(FileReaderManager.getInstance().getConfigReader().getImplicitWait(), TimeUnit.SECONDS);
return driver;
}
public void closeDriver() {
driver.manage().deleteAllCookies();
driver.close();
driver.quit();
}
}
Now we can use WebDriver Manager in our step definition file. After updating the code it should look like this:
package stepDefinitions;
import cucumber.api.java.en.Then;
import managers.PageObjectManager;
import managers.WebDriverManager;
import org.openqa.selenium.WebDriver;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
public class Steps {
WebDriver driver;
HomePage homePage;
ProductListingPage productListingPage;
CartPage cartPage;
CheckoutPage checkoutPage;
PageObjectManager pageObjectManager;
WebDriverManager webDriverManager;
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
webDriverManager = new WebDriverManager();
driver = webDriverManager.getDriver();
pageObjectManager = new PageObjectManager(driver);
homePage = pageObjectManager.getHomePage();
homePage.navigateTo_HomePage();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() throws InterruptedException {
homePage = new HomePage(driver);
Thread.sleep(1000);
homePage.perform_Search("dress");
Thread.sleep(1000);
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() throws InterruptedException {
Thread.sleep(1000);
productListingPage = pageObjectManager.getProductListingPage();
productListingPage.select_Product(0);
productListingPage.makeSelection(1);
productListingPage.clickOn_AddToCart();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() throws InterruptedException{
Thread.sleep(1000);
cartPage = pageObjectManager.getCartPage();
cartPage.clickOn_Cart();
cartPage.clickOn_ContinueToCheckout();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() throws InterruptedException {
Thread.sleep(1000);
checkoutPage = pageObjectManager.getCheckoutPage();
checkoutPage.fill_PersonalDetails();
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.check_TermsAndCondition();
checkoutPage.clickOn_PlaceOrder();
webDriverManager.closeDriver();
}
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
System.out.println("Not implemented");
}
}
Run TestRunner and the test should be executed successfully
Dividing Cucumber Steps between many classes may be a good idea. It is, however, probably not needed early in a project. When we write our first scenario, we will most likely only have just a few steps. The first class with steps is probably small and we can easily find our way around it.
But a scenario in Cucumber is a series of steps which gets executed one after another. Each step may have some state which can be required by other step in the scenario. In other words, some steps may depend on previous steps. This means that we must be able to share state/collected data between steps.
Also when a number of tests grows as project matures, keeping all the steps in a single Step Definition class quickly becomes difficult to manage. To overcome this, different classes for different features / scenarios / steps are used instead.
Now we have a new problem – objects we create in one step class may be needed in the other step classes as well.
Back to our case, till now we just had one scenario which had few steps and we kept all the steps in the same Step definition file. In a real life project there are tens or even hundreds of scenarios and step definition files. And often there is a need to share the Test Context (including Scenario Context, Test State, data collected during test step execution) with all the Step Definition classes. Cucumber supports several Dependency Injection (DI) Containers – it simply tells a DI container to instantiate step definition classes and wire them up correctly. One of the supported DI containers is PicoContainer.
PicoContainer is a small library which doesn’t require set up of any configuration and use of any APIs such as @Inject. It uses constructors instead.
PicoContainer really only has a one functionality – it instantiates objects. Simply hand it some classes and it will instantiate each one, correctly wiring together via their constructors. Cucumber scans your classes with step definitions in them, passes them to PicoContainer, then asks it to create new instances for every scenario.
We will be performing below steps to implement data sharing across steps:
- Add PicoContainer dependency to the Project
- Create a Test Context class which will hold all the objects state
- Divide the Steps class into multiple steps classes with logical separation
- Write Constructor to share Test Context
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>${cucumber-picocontainer.version}</version>
<scope>test</scope>
</dependency>
Note: It is suggested to use the same cucumber-picocontainer version as cucumber version. In our case it is 4.2.0 We also need to create a new property cucumber-picocontainer.version inside tag of pom.xml where we specify picocontainer version:
<properties>
<cucumber.version>4.2.0</cucumber.version>
<cucumber-picocontainer.version>4.2.0</cucumber-picocontainer.version>
</properties>
We should create this class logically: identify all information our Step definition file is using and put that information into this class. In our case our Steps.java is using the following information:
- PageObjects : Provided by PageObjectManager
- WebDriver : Provided by WebDriverManager
- Properties : Provided by FileReaderManager
So, we need the above objects in our Test Context class. Next, if we look at the objects, we see that our FileReaderManager is already a Singleton Class and to use it we don’t need to create an instance of it. It creates it by itself. So no need to add FileReaderManager to TestContext class, as this class can be referred directly statically like
FileReaderManager.getInstance()
-
Create a New Package under src/main/java and name it 'cucumber'. We will keep all the Cucumber Helper classes in the same package moving forward.
-
Create a New Class file and name it 'TestContext'.
-
Add the following code to TestContext file:
package cucumber;
import managers.PageObjectManager;
import managers.WebDriverManager;
public class TestContext {
private WebDriverManager webDriverManager;
private PageObjectManager pageObjectManager;
public TestContext(){
webDriverManager = new WebDriverManager();
pageObjectManager = new PageObjectManager(webDriverManager.getDriver());
}
public WebDriverManager getWebDriverManager() {
return webDriverManager;
}
public PageObjectManager getPageObjectManager() {
return pageObjectManager;
}
}
We kept the initialisation in the constructor and created getMethods() for both objects.
We will divide the steps file as we did the separations between the Page Objects - for every different page we have a separate PageObject class. So it makes sense to have a separate step definition class for every page as well.
- Create four New Classes in the stepDefinitions package with following names:
- HomePageSteps
- ProductPageSteps
- CartPageSteps
- CheckoutPageSteps
Then we start copying-pasting information from steps class into above created classes accordingly.
(Note: We will create ConfirmationPageSteps Class in the following section and for now leave the code for @Then Step in our old step definition file Steps.java)
HomePageSteps.java (after copying step definitions related to HomePage to its own HomePage step definition class and before implementing TextContext)
package stepDefinitions;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import managers.FileReaderManager;
import managers.PageObjectManager;
import managers.WebDriverManager;
import org.openqa.selenium.WebDriver;
import pageObjects.HomePage;
public class HomePageSteps {
WebDriver driver;
HomePage homePage;
PageObjectManager pageObjectManager;
WebDriverManager webDriverManager;
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
webDriverManager = new WebDriverManager();
driver = webDriverManager.getDriver();
pageObjectManager = new PageObjectManager(driver);
homePage = pageObjectManager.getHomePage();
homePage.navigateTo_HomePage();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() throws InterruptedException {
homePage = new HomePage(driver);
Thread.sleep(1000);
homePage.perform_Search("dress");
Thread.sleep(1000);
}
}
Code for other Step definition Classes will follow the same principle
First, lets look at our HomePageSteps file. We need WebDriverManager and PageObjectManager in every step file. Therefore we need to create objects for both classes using new operator again and again.
Now with just adding Constructor to HomePageSteps class and passing TestContext as a Parameter to constructor would take all the pain. Within the TestContext object we have everything available which is required for the test. The new HomePageSteps class should look like this now:
package stepDefinitions;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import managers.FileReaderManager;
import cucumber.TestContext;
import pageObjects.HomePage;
public class HomePageSteps {
HomePage homePage;
TestContext testContext;
//constructor
public HomePageSteps(TestContext context) {
testContext = context;
homePage = testContext.getPageObjectManager().getHomePage();
}
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
homePage.navigateTo_HomePage();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() throws InterruptedException {
homePage.perform_Search("dress");
Thread.sleep(1000);
}
}
Using PicoContainer to share state between steps in a scenario is easy and non intrusive. All we need is a constructor that requires an object that PicoContainer can create and inject. PicoContainer is invisible. Add cucumber-picocontainer dependency and make sure that the constructors for the step classes require an instance of a the same class.
Now we can start modifying rest of the step classes:
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import pageObjects.ProductListingPage;
public class ProductPageSteps {
TestContext testContext;
ProductListingPage productListingPage;
public ProductPageSteps(TestContext context) {
testContext = context;
productListingPage = testContext.getPageObjectManager().getProductListingPage();
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() throws InterruptedException {
Thread.sleep(1000);
productListingPage.select_Product(0);
productListingPage.makeSelection(1);
productListingPage.clickOn_AddToCart();
}
}
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import pageObjects.CartPage;
public class CartPageSteps {
TestContext testContext;
CartPage cartPage;
public CartPageSteps(TestContext context) {
testContext = context;
cartPage = testContext.getPageObjectManager().getCartPage();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() throws InterruptedException{
Thread.sleep(1000);
cartPage.clickOn_Cart();
cartPage.clickOn_ContinueToCheckout();
}
}
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import pageObjects.CheckoutPage;
public class CheckoutPageSteps {
TestContext testContext;
CheckoutPage checkoutPage;
public CheckoutPageSteps(TestContext context) {
testContext = context;
checkoutPage = testContext.getPageObjectManager().getCheckoutPage();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.fill_PersonalDetails();
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.check_TermsAndCondition();
checkoutPage.clickOn_PlaceOrder();
testContext.getWebDriverManager().closeDriver();
}
}
package stepDefinitions;
import cucumber.api.java.en.Then;
public class Steps {
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
System.out.println("Not implemented");
}
}
Run TestRunner and the test should be executed successfully
Unlike TestNG Annotations, Cucumber supports only two hooks (@before and @after). They work at the start and the end of the test scenario. As the name suggests, @Before hook gets executed well before any other test scenario, and @after hook gets executed after executing the scenario.
Implementation of Cucumber Hooks will allow us to to move manipulations with WebDriver (initialisation / closing down) from Page Objects and Step definitions to Hooks Class. In order to achieve the above, we will be performing below steps:
- Create a Hook Class and include GetDriver() and ClosingDriver() methods
- Remove closeDriver() method from CheckoutPageSteps definition file
Note: Currently WebDriver is being initialised when we called navigateTo_HomePage() from our @given Step. This means that technically we may not need to add getDriver() to @before Hooks. But for the purpose of keeping correct structure of the framework and following separation of concern principle, we make sure that the first initialisation of a WebDriver happens in the @before Hook and not during actual test execution
Create a New Class file in src/test/java inside stepDefinitions package and name it 'Hooks' and place the following code inside it
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.After;
import cucumber.api.java.Before;
public class Hooks {
TestContext testContext;
public Hooks(TestContext context) {
testContext = context;
}
@Before
public void BeforeSteps() {
testContext.getWebDriverManager().getDriver();
}
@After
public void AfterSteps() {
testContext.getWebDriverManager().closeDriver();
}
}
@before Hook is now responsible for WebDriver initialisation and every other WebDriver requests by any other classes during the test execution will receive this instance of a driver
@after Hook is responsible for closing the browser after all test have been executed.
CheckoutPageSteps definition file should look like this:
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import pageObjects.CheckoutPage;
public class CheckoutPageSteps {
TestContext testContext;
CheckoutPage checkoutPage;
public CheckoutPageSteps(TestContext context) {
testContext = context;
checkoutPage = testContext.getPageObjectManager().getCheckoutPage();
}
@When("I enter my personal details")
public void i_enter_my_personal_details() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.fill_PersonalDetails();
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.check_TermsAndCondition();
checkoutPage.clickOn_PlaceOrder();
}
}
Run TestRunner and the test should be executed successfully
Starting from Cucumber version 3 abd above brings a new implementation of Data tables. From a Gherkin perspective, nothing has changed. Data tables are supported as earlier. However, implementation needs some adaptation. In our project we will be converting multi-column data table into a custom type
Note: For more information about different DataTables data structures and conversions please follow the below links:
- https://github.com/cucumber/cucumber/tree/master/datatable
- http://www.thinkcode.se/blog/2018/06/30/data-tables-in-cucumber-3
- https://github.com/grasshopper7/cuke3-migrate-datatabletype/blob/master/cuke3-migrate-datatabletype/src/test/java/dataobject/Lecture.java
In order to implement DataTable we will be performing the below steps:
- Change End2End_Test.feature file so Customer details are specified in a DataTable
- Implement new (custom) type which represents DataTable data
- Register a newly created customer type so Cucumber can convert the data table to it
- In CheckoutPage Class, replace fill_PersonalDetails() method with a new method which reads data from DataTable
- Modify Step definition file to reflect DataTable implementation
Our feature file should now look like this:
Feature: Automated End2End Tests
Description: The purpose of this feature is to test End 2 End integration.
Scenario: Customer place an order by purchasing an item from search
Given I am on Home Page
When I search for product in dress category
And I choose to buy the first item
And I move to checkout from mini cart
And I enter my personal details as follows
| first_name |last_name| country | street_address | city |postcode|phone_number|email_address|
|TestAutomation| Opencast| United Kingdom (UK)|Hoults Yard, Walker Road|Newcastle upon Tyne|NE6 3PE |07438862327 |test@test.com|
And place the order
Then Order details are successfully verified
- Create a new package in src/main/java and name it 'testDataTypes'
- Create a New Class file in src/main/java inside testDataTypes package and name it 'CustomerDataType'.
- The new type CustomerDataType now needs to be implemented. This is one possible implementation:
package testDataTypes;
import java.util.Map;
public class CustomerDataType {
private String firstName;
private String lastName;
private String emailAddress;
private String streetAddress;
private String city;
private String postCode;
private String country;
private String phoneNumber;
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmailAddress() { return emailAddress; }
public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; }
public String getStreetAddress() { return streetAddress; }
public void setStreetAddress(String streetAddress) { this.streetAddress = streetAddress; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
public String getPostCode() { return postCode; }
public void setPostCode(String postCode) { this.postCode = postCode; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public String getPhoneNumber() { return phoneNumber; }
public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
public static CustomerDataType customerDetails (Map<String, String> entry) {
CustomerDataType input = new CustomerDataType();
input.setFirstName(entry.get("first_name"));
input.setLastName(entry.get("last_name"));
input.setCountry(entry.get("country"));
input.setStreetAddress(entry.get("street_address"));
input.setCity(entry.get("city"));
input.setPostCode(entry.get("postcode"));
input.setPhoneNumber(entry.get("phone_number"));
input.setEmailAddress(entry.get("email_address"));
return input;
}
@Override
public String toString() {
return "CustomerDataType [" +
"first_name: " + firstName +
", last_name: " + lastName +
", country: " + country +
", street_address:" + streetAddress +
", city: " + city +
", postcode: " + postCode +
", phone_number: " + phoneNumber +
", email_address: " + emailAddress +
"]";
}
}
It is an immutable type with the same number of fields as columns in our DataTable and the fields match the table headers. The fields and headers doesn't have to match. But as they describe the same thing it feels natural that they have the same name in this case. The next step is new for Cucumber version 3 and above. The type has to be registered before it can be used in a data table:
- Create a new class in src/test/java inside stepDefinitions package and name it 'Configurer'
- Place the following code inside it:
package stepDefinitions;
import cucumber.api.TypeRegistry;
import cucumber.api.TypeRegistryConfigurer;
import io.cucumber.datatable.DataTableType;
import io.cucumber.datatable.TableEntryTransformer;
import java.util.Locale;
import java.util.Map;
import testDataTypes.*;
public class Configurer implements TypeRegistryConfigurer {
@Override
public void configureTypeRegistry(TypeRegistry registry) {
registry.defineDataTableType(new DataTableType(CustomerDataType.class, new TableEntryTransformer<CustomerDataType>() {
@Override
public CustomerDataType transform(Map<String, String> entry) {
return CustomerDataType.customerDetails(entry);
}
}));
}
@Override
public Locale locale() {
return Locale.ENGLISH;
}
}
With this infrastructure prepared, it is time to do the actual implementation of the steps that uses this data table:
Step 4: In CheckoutPage Class, replace fill_PersonalDetails() method with a new method which reads data from DataTable
CheckoutPage should look like this:
package pageObjects;
import org.junit.Assert;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.Select;
import testDataTypes.CustomerDataType;
import java.util.ArrayList;
import java.util.List;
public class CheckoutPage {
public CheckoutPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
@FindBy(id = "billing_first_name")
public WebElement txtbx_FirstName;
@FindBy(id = "billing_last_name")
public WebElement txtbx_LastName;
@FindBy(id = "billing_email")
public WebElement txtbx_Email;
@FindBy(id = "billing_phone")
public WebElement txtbx_Phone;
@FindBy(id = "billing_country")
public WebElement select_Country;
@FindBy(id = "billing_city")
public WebElement txtbx_City;
@FindBy(id = "billing_address_1")
public WebElement txtbx_Address;
@FindBy(css = "#billing_postcode")
public WebElement txtbx_PostCode;
@FindBy(css = ".woocommerce-form__input-checkbox")
public WebElement chkbx_AcceptTermsAndCondition;
@FindBy(id = "place_order")
public WebElement btn_PlaceOrder;
public void select_Country(String countryName) {
Select country = new Select(select_Country);
country.selectByVisibleText(countryName);
}
public void check_TermsAndCondition() {
chkbx_AcceptTermsAndCondition.click();
}
public void clickOn_PlaceOrder() {
btn_PlaceOrder.submit();
}
public void CustomerPersonalDetailsFromDataTable(List<CustomerDataType> inputs) {
try {
//Creating arrays for each table column header
List<String> firstNameArr = new ArrayList<>();
List<String> lastNameArr = new ArrayList<>();
List<String> countryArr = new ArrayList<>();
List<String> streetAddressArr = new ArrayList<>();
List<String> cityArr = new ArrayList<>();
List<String> postcodeArr = new ArrayList<>();
List<String> phoneNumberArr = new ArrayList<>();
List<String> emailAddressArr = new ArrayList<>();
Integer size = inputs.size();
//Recording data from table rows into corresponding data arrays
for (CustomerDataType input : inputs) {
firstNameArr.add(input.getFirstName());
lastNameArr.add(input.getLastName());
countryArr.add(input.getCountry());
streetAddressArr.add(input.getStreetAddress());
cityArr.add(input.getCity());
postcodeArr.add(input.getPostCode());
phoneNumberArr.add(input.getPhoneNumber());
emailAddressArr.add(input.getEmailAddress());
}
//Assigning DataTable data to corresponding variables. We use these values in .sendKey() method
for (int i = 0; i < size; i++) {
String firstNameKey = firstNameArr.get(i);
String lastNameKey = lastNameArr.get(i);
String countryKey = countryArr.get(i);
String streetAddressKey = streetAddressArr.get(i);
String cityKey = cityArr.get(i);
String postcodeKey = postcodeArr.get(i);
String phoneNumberKey = phoneNumberArr.get(i);
String emailAddressKey = emailAddressArr.get(i);
txtbx_FirstName.sendKeys(firstNameKey);
txtbx_LastName.sendKeys(lastNameKey);
select_Country(countryKey);
txtbx_Address.sendKeys(streetAddressKey);
txtbx_City.sendKeys(cityKey);
txtbx_PostCode.sendKeys(postcodeKey);
Thread.sleep(2000);
txtbx_Phone.sendKeys(phoneNumberKey);
txtbx_Email.sendKeys(emailAddressKey);
}
} catch (Exception e) {
Assert.fail("Unable to to locate WebElement or/and send keys to it, Exception: " + e.getMessage());
}
}
}
- Run TestRunner to get our modified step code snippet:
@When("I enter my personal details as follows")
public void i_enter_my_personal_details_as_follows(io.cucumber.datatable.DataTable dataTable) {
// Write code here that turns the phrase above into concrete actions
// For automatic transformation, change DataTable to one of
// E, List<E>, List<List<E>>, List<Map<K,V>>, Map<K,V> or
// Map<K, List<V>>. E,K,V must be a String, Integer, Float,
// Double, Byte, Short, Long, BigInteger or BigDecimal.
//
// For other transformations you can register a DataTableType.
throw new cucumber.api.PendingException();
}
- Inside the step body include CustomerPersonalDetailsFromDataTable(List inputs) method invocation. CheckoutPageSteps should look look this:
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import pageObjects.CheckoutPage;
import testDataTypes.CustomerDataType;
import java.util.List;
public class CheckoutPageSteps {
TestContext testContext;
CheckoutPage checkoutPage;
public CheckoutPageSteps(TestContext context) {
testContext = context;
checkoutPage = testContext.getPageObjectManager().getCheckoutPage();
}
@When("I enter my personal details as follows")
public void i_enter_my_personal_details_as_follows(List<CustomerDataType> inputs) throws InterruptedException {
Thread.sleep(1000);
checkoutPage.CustomerPersonalDetailsFromDataTable(inputs);
}
@When("I place the order")
public void i_place_the_order() throws InterruptedException {
Thread.sleep(1000);
checkoutPage.check_TermsAndCondition();
checkoutPage.clickOn_PlaceOrder();
}
}
Run TestRunner and the test should be executed successfully
In Selenium "Waits" play an important role in executing tests.
Most of the Web applications are developed using Ajax and Javascript. When a page is loaded by the browser the elements which we want to interact with may load at different time intervals. Not only it makes this difficult to identify the element but also if the element is not located it will throw an "ElementNotVisibleException" exception. Using Waits, we can resolve this problem.
- Implicit Wait
- Explicit Wait
Selenium Web Driver has borrowed the idea of implicit waits from Watir.
The implicit wait will tell to the web driver to wait for certain amount of time before it throws a "No Such Element Exception". The default setting is 0. Once we set the time, web driver will wait for that time before throwing an exception. We have Implicit Wait implementation in our project inside WebDriver Manager class when we are waiting for the App URL to load:
driver.manage().timeouts().implicitlyWait(FileReaderManager.getInstance().getConfigReader().getImplicitWait(), TimeUnit.SECONDS);
The explicit wait is used to tell the Web Driver to wait for certain conditions (Expected Conditions) or the maximum time exceeded before throwing an "ElementNotVisibleException" exception. The explicit wait is an intelligent kind of wait, but it can be applied only for specified elements. Explicit wait gives better options than an implicit wait as it will wait for dynamically loaded Ajax elements. Once we declare explicit wait we have to use "ExpectedConditions".
So far in our project we are using Thread.Sleep(). This generally is not recommended to use. Only in exeptional conditions, when there is no other options to work around a particular execution, we can use it. Thread.Sleep() significantly slows down test execution. Execution will just stops for specified in Thread.sleep()statement time before continue with the next statement. In order to make our test execution more time efficient, we are going to implement a couple of Explicit Wait methods and custom specified timeout time. In order to implement Explicit Wait we will be performing the below steps:
- Create a New utility class where all Wait methods will be placed
- Add explicitWait variable into Configuration.properties file
- Modify getImplicitWait method so it reads both custom Explicit and Implicit Wait value by taken a 'Key' as a parameter
- Include Wait methods inside our project's PageObject classes
- Modify Step definition files to reflect the changes
- Modify createLocalDriver() method inside WebDriver Manager to add implicitWait parameter to it
- Create a new package in src/main/java and name it 'utils'
- Create a New Class file in src/main/java inside 'utils' package and name it 'Waits'. We will add two Explicit Wait methods to our Wait Class:
- ExpectedConditions.elementToBeClickable(element))
- ExpectedConditions.visibilityOf(element) New Waits Class should look like this:
package utils;
import org.junit.Assert;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class Waits {
public boolean WaitForClickableWithCustomTimeout(WebDriver driver, WebElement element, long customTimeout) {
try {
final WebDriverWait customWait;
customWait= new WebDriverWait(driver, 10);
customWait.until(ExpectedConditions.elementToBeClickable(element));
System.out.println("Element is clickable, locator: " + "<" + element + ">" + ", custom Timeout: " + customTimeout);
return true;
} catch (Exception e) {
System.out.println("Unable to click on WebElement, locator: " + "<" + element.getClass() + ">" + ", custom Timeout: " + customTimeout);
Assert.fail("Unable to click on WebElement, Exception: " + e.getMessage());
return false;
}
}
public boolean WaitForVisibleWithCustomTimeout(WebDriver driver, WebElement element, long customTimeout) {
try {
final WebDriverWait customWait;
customWait= new WebDriverWait(driver, customTimeout);
customWait.until(ExpectedConditions.visibilityOf(element));
System.out.println("Successfully found WebElement, locator: " + "<" + element + ">" + ", custom Timeout: " + customTimeout);
return true;
} catch (Exception e) {
System.out.println("Unable to find WebElement, locator: " + "<" + element + ">" + ", custom Timeout: " + customTimeout);
Assert.fail("Unable to find WebElement, Exception: " + e.getMessage());
return false;
}
}
}
Our Configuration.properties file should look like this:
environment=local
browser=chrome
windowMaximize=true
driverPath=src/drivers/chromedriver
url=http://shop.demoqa.com
implicitWait=10
explicitWait=5
Inside scr/main/java/dataProviders/ConfigFileReader we will modify our getImplicitWait() method so it can read both custom Implicit and Explicit Wait values. We give it a more meaningful name too:
public long getCustomWait(String waitTypeKey) {
String customWait = properties.getProperty(waitTypeKey);
if(customWait != null) return Long.parseLong(customWait);
else throw new RuntimeException("Custom Wait not specified in the Configuration.properties file.");
}
package dataProviders;
import enums.DriverType;
import enums.EnvironmentType;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class ConfigFileReader {
private Properties properties;
private final String propertyFilePath= "configs/Configuration.properties";
public ConfigFileReader(){
BufferedReader reader;
try {
reader = new BufferedReader(new FileReader(propertyFilePath));
//Configuration properties can be easily read from .properties file using object of type Properties provided by java.utils
properties = new Properties();
try {
properties.load(reader);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
throw new RuntimeException("Configuration.properties not found at " + propertyFilePath);
}
}
public String getDriverPath(){
String driverPath = properties.getProperty("driverPath");
if(driverPath!= null) return driverPath;
else throw new RuntimeException("driverPath not specified in the Configuration.properties file.");
}
public long getCustomWait(String waitTypeKey) {
String customWait = properties.getProperty(waitTypeKey);
if(customWait != null) return Long.parseLong(customWait);
else throw new RuntimeException("Custom Wait not specified in the Configuration.properties file.");
}
public String getApplicationUrl() {
String url = properties.getProperty("url");
if(url != null) return url;
else throw new RuntimeException("url not specified in the Configuration.properties file.");
}
public DriverType getBrowser() {
String browserName = properties.getProperty("browser");
if(browserName == null || browserName.equals("chrome")) return DriverType.CHROME;
else if(browserName.equalsIgnoreCase("firefox")) return DriverType.FIREFOX;
else throw new RuntimeException("Browser Name Key value in Configuration.properties is not matched : " + browserName);
}
public EnvironmentType getEnvironment() {
String environmentName = properties.getProperty("environment");
if(environmentName == null || environmentName.equalsIgnoreCase("local")) return EnvironmentType.LOCAL;
else if(environmentName.equals("remote")) return EnvironmentType.REMOTE;
else throw new RuntimeException("Environment Type Key value in Configuration.properties is not matched : " + environmentName);
}
public Boolean getBrowserWindowSize() {
String windowSize = properties.getProperty("windowMaximize");
if(windowSize != null) return Boolean.valueOf(windowSize);
return true;
}
}
package pageObjects;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import utils.Waits;
public class HomePage {
WebDriver driver;
Waits wait;
public HomePage(WebDriver driver) {
this.driver = driver;
wait = new Waits();
PageFactory.initElements(driver, this);
}
@FindBy(css=".noo-search")
public WebElement btn_Search;
@FindBy(css=".form-control")
public WebElement input_Search;
public void navigateTo_HomePage() {
driver.get(FileReaderManager.getInstance().getConfigReader().getApplicationUrl());
}
public void perform_Search(String search, long customTimeout) {
if(wait.WaitForVisibleWithCustomTimeout(driver,btn_Search, customTimeout)) {
btn_Search.click();
input_Search.sendKeys(search);
input_Search.sendKeys(Keys.RETURN);
}
}
}
package pageObjects;
import java.util.List;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.Select;
import utils.Waits;
public class ProductListingPage {
WebDriver driver;
Waits wait;
public ProductListingPage(WebDriver driver) {
this.driver = driver;
wait = new Waits();
PageFactory.initElements(driver, this);
}
@FindBy(css = "button.single_add_to_cart_button")
public WebElement btn_AddToCart;
@FindAll(@FindBy(css = ".noo-product-inner"))
public List<WebElement> prd_List;
@FindBy(id="pa_color")
public WebElement selectColour;
@FindBy(id="pa_size")
public WebElement selectSize;
public void select_Product(int productNumber, long customTimeout) {
if (wait.WaitForVisibleWithCustomTimeout(driver,prd_List.get(productNumber), customTimeout)) {
prd_List.get(productNumber).click();
}
}
public void makeSelection(int index, long customTimeout) {
if (wait.WaitForVisibleWithCustomTimeout(driver,selectColour, customTimeout)) {
Select colour = new Select(selectColour);
colour.selectByIndex(index);
Select size = new Select(selectSize);
size.selectByIndex(index);
}
}
public void clickOn_AddToCart(long customTimeout) {
if (wait.WaitForClickableWithCustomTimeout(driver,btn_AddToCart, customTimeout)) {
btn_AddToCart.click();
}
}
}
package pageObjects;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import utils.Waits;
public class CartPage {
WebDriver driver;
Waits wait;
public CartPage(WebDriver driver) {
this.driver = driver;
wait = new Waits();
PageFactory.initElements(driver, this);
}
@FindBy(css = ".cart-button")
public WebElement btn_Cart;
@FindBy(css = ".checkout-button.alt")
public WebElement btn_ContinueToCheckout;
public void clickOn_Cart(long customTimeout) {
if(wait.WaitForVisibleWithCustomTimeout(driver,btn_Cart, customTimeout)) {
btn_Cart.click();
}
}
public void clickOn_ContinueToCheckout(long customTimeout){
if(wait.WaitForVisibleWithCustomTimeout(driver,btn_ContinueToCheckout, customTimeout)) {
btn_ContinueToCheckout.click();
}
}
}
package pageObjects;
import org.junit.Assert;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.Select;
import testDataTypes.CustomerDataType;
import utils.Waits;
import java.util.ArrayList;
import java.util.List;
public class CheckoutPage {
WebDriver driver;
Waits wait;
public CheckoutPage(WebDriver driver) {
this.driver = driver;
wait = new Waits();
PageFactory.initElements(driver, this);
}
@FindBy(id = "billing_first_name")
public WebElement txtbx_FirstName;
@FindBy(id = "billing_last_name")
public WebElement txtbx_LastName;
@FindBy(id = "billing_email")
public WebElement txtbx_Email;
@FindBy(id = "billing_phone")
public WebElement txtbx_Phone;
@FindBy(id = "billing_country")
public WebElement select_Country;
@FindBy(id = "billing_city")
public WebElement txtbx_City;
@FindBy(id = "billing_address_1")
public WebElement txtbx_Address;
@FindBy(css = "#billing_postcode")
public WebElement txtbx_PostCode;
@FindBy(css = ".woocommerce-form__input-checkbox")
public WebElement chkbx_AcceptTermsAndCondition;
@FindBy(id = "place_order")
public WebElement btn_PlaceOrder;
@FindBy(id = "order_review")
public WebElement test;
public void select_Country(String countryName) {
Select country = new Select(select_Country);
country.selectByVisibleText(countryName);
}
public void check_TermsAndCondition(long customTimeout) {
if(wait.WaitForClickableWithCustomTimeout(driver,chkbx_AcceptTermsAndCondition, customTimeout)) {
chkbx_AcceptTermsAndCondition.click();
}
}
public void clickOn_PlaceOrder(long customTimeout) {
if(wait.WaitForClickableWithCustomTimeout(driver,chkbx_AcceptTermsAndCondition, customTimeout)) {
btn_PlaceOrder.submit();
}
}
public void CustomerPersonalDetailsFromDataTable(List<CustomerDataType> inputs, long customTimeout) {
try {
//Creating arrays for each table column header
List<String> firstNameArr = new ArrayList<>();
List<String> lastNameArr = new ArrayList<>();
List<String> countryArr = new ArrayList<>();
List<String> streetAddressArr = new ArrayList<>();
List<String> cityArr = new ArrayList<>();
List<String> postcodeArr = new ArrayList<>();
List<String> phoneNumberArr = new ArrayList<>();
List<String> emailAddressArr = new ArrayList<>();
Integer size = inputs.size();
//Recording data from table rows into corresponding data arrays
for (CustomerDataType input : inputs) {
firstNameArr.add(input.getFirstName());
lastNameArr.add(input.getLastName());
countryArr.add(input.getCountry());
streetAddressArr.add(input.getStreetAddress());
cityArr.add(input.getCity());
postcodeArr.add(input.getPostCode());
phoneNumberArr.add(input.getPhoneNumber());
emailAddressArr.add(input.getEmailAddress());
}
//Assigning DataTable data to corresponding variables. We use these values in .sendKey() method
for (int i = 0; i < size; i++) {
String firstNameKey = firstNameArr.get(i);
String lastNameKey = lastNameArr.get(i);
String countryKey = countryArr.get(i);
String streetAddressKey = streetAddressArr.get(i);
String cityKey = cityArr.get(i);
String postcodeKey = postcodeArr.get(i);
String phoneNumberKey = phoneNumberArr.get(i);
String emailAddressKey = emailAddressArr.get(i);
if(wait.WaitForVisibleWithCustomTimeout(driver,test, customTimeout)) {
txtbx_FirstName.sendKeys(firstNameKey);
txtbx_LastName.sendKeys(lastNameKey);
select_Country(countryKey);
txtbx_Address.sendKeys(streetAddressKey);
txtbx_City.sendKeys(cityKey);
txtbx_PostCode.sendKeys(postcodeKey);
Thread.sleep(2000);
txtbx_Phone.sendKeys(phoneNumberKey);
txtbx_Email.sendKeys(emailAddressKey);
}
}
} catch (Exception e) {
Assert.fail("Unable to to locate WebElement or/and send keys to it, Exception: " + e.getMessage());
}
}
}
package stepDefinitions;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.When;
import managers.FileReaderManager;
import cucumber.TestContext;
import pageObjects.HomePage;
public class HomePageSteps {
HomePage homePage;
TestContext testContext;
String url = FileReaderManager.getInstance().getConfigReader().getApplicationUrl();
long customTimeout = FileReaderManager.getInstance().getConfigReader().getCustomWait("explicitWait");
//constructor
public HomePageSteps(TestContext context) {
testContext = context;
homePage = testContext.getPageObjectManager().getHomePage();
}
@Given("I am on Home Page")
public void i_am_on_Home_Page() {
homePage.navigateTo_HomePage();
}
@When("I search for product in dress category")
public void i_search_for_product_in_dress_category() {
homePage.perform_Search("dress", customTimeout);
}
}
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import managers.FileReaderManager;
import pageObjects.ProductListingPage;
public class ProductPageSteps {
TestContext testContext;
ProductListingPage productListingPage;
long customTimeout = FileReaderManager.getInstance().getConfigReader().getCustomWait("explicitWait");
public ProductPageSteps(TestContext context) {
testContext = context;
productListingPage = testContext.getPageObjectManager().getProductListingPage();
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() throws InterruptedException {
productListingPage.select_Product(0, customTimeout);
productListingPage.makeSelection(1, customTimeout);
productListingPage.clickOn_AddToCart(customTimeout);
}
}
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import managers.FileReaderManager;
import pageObjects.CartPage;
public class CartPageSteps {
TestContext testContext;
CartPage cartPage;
long customTimeout = FileReaderManager.getInstance().getConfigReader().getCustomWait("explicitWait");
public CartPageSteps(TestContext context) {
testContext = context;
cartPage = testContext.getPageObjectManager().getCartPage();
}
@When("I move to checkout from mini cart")
public void i_move_to_checkout_from_mini_cart() throws InterruptedException{
Thread.sleep(1000);
cartPage.clickOn_Cart(customTimeout);
cartPage.clickOn_ContinueToCheckout(customTimeout);
}
}
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import managers.FileReaderManager;
import pageObjects.CheckoutPage;
import testDataTypes.CustomerDataType;
import java.util.List;
public class CheckoutPageSteps {
TestContext testContext;
CheckoutPage checkoutPage;
long customTimeout = FileReaderManager.getInstance().getConfigReader().getCustomWait("explicitWait");
public CheckoutPageSteps(TestContext context) {
testContext = context;
checkoutPage = testContext.getPageObjectManager().getCheckoutPage();
}
@When("I enter my personal details as follows")
public void i_enter_my_personal_details_as_follows(List<CustomerDataType> inputs) {
checkoutPage.CustomerPersonalDetailsFromDataTable(inputs, customTimeout);
}
@When("I place the order")
public void i_place_the_order() {
checkoutPage.check_TermsAndCondition(customTimeout);
checkoutPage.clickOn_PlaceOrder(customTimeout);
}
}
Step 6: Modify createLocalDriver() method inside WebDriver Manager class to add implicitWait parameter to it
private WebDriver createLocalDriver() {
switch (driverType) {
case FIREFOX : driver = new FirefoxDriver();
break;
case CHROME :
System.setProperty(CHROME_DRIVER_PROPERTY, FileReaderManager.getInstance().getConfigReader().getDriverPath());
driver = new ChromeDriver();
break;
}
if(FileReaderManager.getInstance().getConfigReader().getBrowserWindowSize()) driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(FileReaderManager.getInstance().getConfigReader().getCustomWait("implicitWait"), TimeUnit.SECONDS);
return driver;
}
Run TestRunner and test should be executed successfully
This section is about how we can share Test Context between the Cucumber Steps.
During test scenario execution there might be situations where we need to carry a data or a state from one step to another.
This data or state can be within these two levels:
- Test Context
- Scenario Context So what is Test Context and Scenario Context and what is the difference between two.
TestContext is the parent class and the medium to share the information between the different steps in a test.
It can have many class objects in it. If we go back to the section 7 of this tutorial where Test Context was first introduced, we can see that it already has PageObjectManager and WebDriverManager objects in it.
Scenario Context is a class which specifically designed to hold test data information. It actually uses the Test Context class to travel the information between various steps. Within this ScenarioContext class, we can create any number of fields to store any form of data. It stores the information in the key-value pair and again, value can be of any type. Also the important point here is that the information which we store in Scenario Context is generated at the run time. This means that during the execution, if we wish to store some information, we will use Scenario Context.
Structure of the TestContext class will be like this:
public class TestContext {
WebDriverManager webDriverManager;
PageObjectManager pageObjectManager;
ScenarioContext scenarioContext;
}
Let’s get back to our Test Scenario and implement a validation. Lets say we want to validate that the Name of the Product displayed on the order confirmation page is the same as for the Product we added to the cart. In that case we need to store the name of the product in Scenario Context object at the adding product to the cart step. We will retrieve the product from ScenarioContext and validate the name on the final confirmation page after the order placement is done.
- Create a new enum in src/main/java inside 'enums' package and name it 'Context'.
package enums;
public enum Context {
PRODUCT_NAME;
}
- Create a new class in src/main/java inside 'testDataTypes package and name it 'ScenarioContext'
package testDataTypes;
import java.util.HashMap;
import java.util.Map;
import enums.Context;
public class ScenarioContext {
private Map<String, Object> scenarioContext;
public ScenarioContext(){
scenarioContext = new HashMap<>();
}
public void setContext(Context key, Object value) {
scenarioContext.put(key.toString(), value);
}
public Object getContext(Context key){
return scenarioContext.get(key.toString());
}
public Boolean isContains(Context key){
return scenarioContext.containsKey(key.toString());
}
}
scenarioContext : This is a HasMap object which stores the information in the Key-Value pair. Key type is String and Value can be of any Object Type.
setContext() : This method takes two parameters, key as String and value as object. Key is nothing but a Context enum.
getContext() : This method takes key as parameter and returned the object which match the key.
isContains() : This method performs a check on the complete Map that if it contains the key or not.
- Include ScenarioContext in TextContext class, so that it can be shared across all the Cucumber Steps using Picocontainer library. Also, inside the TestContext class we need to add a 'getter' method getScenarioContext() to get the scenarioContext object. TestContext class will look like this:
package cucumber;
import managers.PageObjectManager;
import managers.WebDriverManager;
import testDataTypes.ScenarioContext;
public class TestContext {
private WebDriverManager webDriverManager;
private PageObjectManager pageObjectManager;
public ScenarioContext scenarioContext;
public TestContext(){
webDriverManager = new WebDriverManager();
pageObjectManager = new PageObjectManager(webDriverManager.getDriver());
scenarioContext = new ScenarioContext();
}
public WebDriverManager getWebDriverManager() {
return webDriverManager;
}
public PageObjectManager getPageObjectManager() {
return pageObjectManager;
}
public ScenarioContext getScenarioContext() {
return scenarioContext;
}
}
To use the value of the product Name later in the test for validation, we need to save it as a part of 'I choose to buy the first item' step.
- Add a new getProductName() method in the ProductListingPage class which will return the Name of the Product:
public String getProductName(long customTimeout) {
String productName;
if (wait.WaitForVisibleWithCustomTimeout(driver,selectedProduct, customTimeout)) {
productName = selectedProduct.findElement(By.cssSelector("h1")).getText();
} else {
productName = "Unable to get Product Name";
}
return productName;
}
package pageObjects;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.Select;
import utils.Waits;
public class ProductListingPage {
WebDriver driver;
Waits wait;
public ProductListingPage(WebDriver driver) {
this.driver = driver;
wait = new Waits();
PageFactory.initElements(driver, this);
}
@FindBy(css = "button.single_add_to_cart_button")
public WebElement btn_AddToCart;
@FindAll(@FindBy(css = ".noo-product-inner"))
public List<WebElement> prd_List;
@FindBy(id="pa_color")
public WebElement selectColour;
@FindBy(id="pa_size")
public WebElement selectSize;
@FindBy(css= ".entry-summary")
public WebElement selectedProduct;
public void select_Product(int productNumber, long customTimeout) {
if (wait.WaitForVisibleWithCustomTimeout(driver,prd_List.get(productNumber), customTimeout)) {
prd_List.get(productNumber).click();
}
}
public void makeSelection(int index, long customTimeout) {
if (wait.WaitForVisibleWithCustomTimeout(driver,selectColour, customTimeout)) {
Select colour = new Select(selectColour);
colour.selectByIndex(index);
Select size = new Select(selectSize);
size.selectByIndex(index);
}
}
public void clickOn_AddToCart(long customTimeout) {
if (wait.WaitForClickableWithCustomTimeout(driver,btn_AddToCart, customTimeout)) {
btn_AddToCart.click();
}
}
public String getProductName(long customTimeout) {
String productName;
if (wait.WaitForVisibleWithCustomTimeout(driver,selectedProduct, customTimeout)) {
productName = selectedProduct.findElement(By.cssSelector("h1")).getText();
} else {
productName = "Unable to get Product Name";
}
return productName;
}
}
- Now, using the getProductName() method, get the name and save it into the scenarioContext object in ProductPageSteps class
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import enums.Context;
import managers.FileReaderManager;
import pageObjects.ProductListingPage;
public class ProductPageSteps {
TestContext testContext;
ProductListingPage productListingPage;
long customTimeout = FileReaderManager.getInstance().getConfigReader().getCustomWait("explicitWait");
public ProductPageSteps(TestContext context) {
testContext = context;
productListingPage = testContext.getPageObjectManager().getProductListingPage();
}
@When("I choose to buy the first item")
public void i_choose_to_buy_the_first_item() {
productListingPage.select_Product(0, customTimeout);
String productName = productListingPage.getProductName(customTimeout);
testContext.scenarioContext.setContext(Context.PRODUCT_NAME, productName);
productListingPage.makeSelection(1, customTimeout );
productListingPage.clickOn_AddToCart(customTimeout);
}
}
- Add getProductNames() method to ConfirmationPage Class. This method will retrieve names of all products added to the cart:
package pageObjects;
import java.util.ArrayList;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import utils.Waits;
public class ConfirmationPage {
WebDriver driver;
Waits wait;
public ConfirmationPage(WebDriver driver) {
this.driver = driver;
wait = new Waits();
PageFactory.initElements(driver, this);
}
@FindAll(@FindBy(css = ".order_item"))
public List<WebElement> prd_List;
@FindBy(css = ".woocommerce-order-details")
public WebElement orderDetails;
public List<String> getProductNames(long customTimeout) {
List<String> productNames = new ArrayList<>();
if (wait.WaitForVisibleWithCustomTimeout(driver, orderDetails, customTimeout)) {
for (WebElement element : prd_List) {
productNames.add(element.findElement(By.cssSelector(".product-name")).getText());
}
}
return productNames;
}
}
We now finally ready to move @Then step code from the old Setps.java definition file to the class of its own: ConfirmationPageSteps. But first we need to initialise ConfirmationPage class inside PageObjectManager class:
- Add a new getConfirmationPage() method to get the Confirmation Page object in the PageObjectManager class.
package managers;
import org.openqa.selenium.WebDriver;
import pageObjects.CartPage;
import pageObjects.CheckoutPage;
import pageObjects.ConfirmationPage;
import pageObjects.HomePage;
import pageObjects.ProductListingPage;
public class PageObjectManager {
private WebDriver driver;
private ProductListingPage productListingPage;
private CartPage cartPage;
private HomePage homePage;
private CheckoutPage checkoutPage;
private ConfirmationPage confirmationPage;
// Constructor
public PageObjectManager(WebDriver driver) {
this.driver = driver;
}
public HomePage getHomePage(){
return (homePage == null) ? homePage = new HomePage(driver) : homePage;
}
public ProductListingPage getProductListingPage() {
return (productListingPage == null) ? productListingPage = new ProductListingPage(driver) : productListingPage;
}
public CartPage getCartPage() {
return (cartPage == null) ? cartPage = new CartPage(driver) : cartPage;
}
public CheckoutPage getCheckoutPage() {
return (checkoutPage == null) ? checkoutPage = new CheckoutPage(driver) : checkoutPage;
}
public ConfirmationPage getConfirmationPage() {
return (confirmationPage == null) ? confirmationPage = new ConfirmationPage(driver) : confirmationPage;
}
}
- Now move our code for Confirmation page step from the old Steps class file to ConfirmationPageSteps class like so:
package stepDefinitions;
import managers.FileReaderManager;
import org.junit.Assert;
import cucumber.TestContext;
import cucumber.api.java.en.Then;
import enums.Context;
import pageObjects.ConfirmationPage;
public class ConfirmationPageSteps {
TestContext testContext;
ConfirmationPage confirmationPage;
public long customTimeout;
public ConfirmationPageSteps(TestContext context) {
testContext = context;
confirmationPage = testContext.getPageObjectManager().getConfirmationPage();
customTimeout = FileReaderManager.getInstance().getConfigReader().getCustomWait("explicitWait");
}
@Then("Order details are successfully verified")
public void order_details_are_successfully_verified() {
String productName = ((String) testContext.scenarioContext.getContext(Context.PRODUCT_NAME)).toLowerCase();
Assert.assertTrue(confirmationPage.getProductNames(customTimeout).stream().filter(x -> x.contains(productName)).findFirst().get().length() > 0);
}
}
- Delete old Steps.java file from scr/test/java/stepDefinitions package
Run TestRunner and test should be executed successfully
Data that is external to functional tests is loaded and used to extend automated test cases. In order to demonstrate data driven testing in practice we will make a use of entering customer personal details step again (same step we used when covered DataTables few sections back). We currently use data table to read the data from. This is also a data driven testing of DDT, however the data is specified directly in our feature file. This is perfectly fine, but what if in real word project we need to use large data sample or apply different data sets to the same test scenario? To be sure that the application works as expected with different sets of input data and for more effective management of test data data driven testing with external data source comes into play.
There a few different ways of doing Data Driven Testing with Cucumber:
- Parameterization in Cucumber
- reading data from external file using Examples Keyword
- Data Tables in Cucumber
Out of above, we will use the Data Driven Technique using Example Keywords in our below example. And we will be using JSON to provide Data to our test.
JSON is short for JavaScript Object Notation, and is a way to store information in an organized, easy-to-access manner. It gives us a human-readable collection of data that we can access in a logical manner.
Excel is good to manage data and to use but it comes with its own limitations. Like MS Office needs to be installed on the system where the tests are being executed. This is a big limitation on its own, as the test servers has never bound to have such dependencies. If tests are meant to run on Mac, this is also a problem.
We have to do a lot of changes in our project in this chapter in order to implement Data Driven Technique using JSON files:
- Add another senario which will be using JSON file as a data source
- Create JSON Data set
- Write a Java POJO class to represent JSON data
- Pass JSON data file location to Properties file and Write a method to read that location
- Create a JSON Data Reader class
- Modify FileReaderManager to accommodate JSON Data Reader
- Modify Checkout Page object to use Test Data object
- Modify Checkout Steps file to pass Test Data to Checkout Page Objects
For simplicity we will be using the same scenario we already have We will implement differently a step when customer details need to be entered at the checkout point so it takes data from our JSON file. we also will make it Scenario Outline type of Scenario. This will allow run the same test multiple time and every time with different set of data. Our Feature file should now looks like this:
Feature: Automated End2End Tests
Description: The purpose of this feature is to test End 2 End integration.
Scenario: Customer place an order by purchasing an item from search
Given I am on Home Page
When I search for product in dress category
And I choose to buy the first item
And I move to checkout from mini cart
And I enter my personal details as follows
| first_name |last_name| country | street_address | city |postcode|phone_number|email_address|
|TestAutomation| Opencast| United Kingdom (UK)|Hoults Yard, Walker Road|Newcastle upon Tyne|NE6 3PE |07438862327 |test@test.com|
And I place the order
Then Order details are successfully verified
Scenario Outline: Customer place an order by purchasing an item from search - customer details are taken from JSON file
Given I am on Home Page
When I search for product in dress category
And I choose to buy the first item
And I move to checkout from mini cart
And I enter <customer> personal details
And I place the order
Then Order details are successfully verified
Examples:
| customer |
| Opencast |
| Testuser |
As we can see, we using same steps in our both scenarios up to the step And I enter personal details We already know that code duplication is not a good practice so it is a great opportunity to use 'Background' keyword here to make our Scenario look better. We will put all same for both Scenarios steps inside 'Background' block':
Feature: Automated End2End Tests
Description: The purpose of this feature is to test End 2 End integration.
Background:
Given I am on Home Page
When I search for product in dress category
And I choose to buy the first item
And I move to checkout from mini cart
Scenario: Customer place an order by purchasing an item from search
And I enter my personal details as follows
| first_name |last_name| country | street_address | city |postcode|phone_number|email_address|
|TestAutomation| Opencast| United Kingdom (UK)|Hoults Yard, Walker Road|Newcastle upon Tyne|NE6 3PE |07438862327 |test@test.com|
And I place the order
Then Order details are successfully verified
Scenario Outline: Customer place an order by purchasing an item from search - customer details are taken from JSON file
And I enter <customer> personal details
And I place the order
Then Order details are successfully verified
Examples:
| customer |
| Opencast |
| Testuser |
So far we just passed Customer name from feature file, but we need a other customer details to pass to checkout page to complete the order. These details we will get from JSON file. We ask JSON file to give us the details of any particular Customer out of the all Customers Data. As we need multiple customer data we need to create JSON Data in Arrays.
-
Create a New Package under src/test/resources and name it 'testDataResources. As we are keeping all our test resources in the src/test/resources folder, it is logical we create a new package inside this folder for all our JSON files.
-
Create a New File inside testDataResources package and name it is 'Customer.json'
[
{
"firstName": "Opencast",
"lastName": "Software",
"age": 30,
"emailAddress": "opencast@mail.com",
"address": {
"streetAddress": "Hoults Yard, Walker Road",
"city": "Newcastle upon Tyne",
"postCode": "NE6 3PE",
"state": "n/a",
"country": "United Kingdom (UK)",
"county": "Tyne and Wear"
},
"phoneNumber": {
"home": "012345678",
"mob": "0987654321"
}
},
{
"firstName": "Testuser",
"lastName": "Automation",
"age": 35,
"emailAddress": "test@Gmail.com",
"address": {
"streetAddress": "1 Summer Gardens",
"city": "Sunderland",
"postCode": "SR12 7PE",
"state": "n/a",
"country": "United Kingdom (UK)",
"county": "Tyne and Wear"
},
"phoneNumber": {
"home": "056772211",
"mob": "0772244180"
}
}
]
To use this JSON data in the test we need to first deserializes JSON into an object of the specified class. And to have the JSON deserialized, a java class object must be created that has the same fields names as the fields in the JSON string.
- Create a New Class in scr/main/java under 'testDataType package and name it is 'Customer'
package testDataTypes;
// This is a Java POJO class to represent JSON data
// To use this JSON data in the test we need to first deserializes the JSON into an object of the specified class
// And to have the JSON deserialized, a java class object must be created that has the same fields names with the fields in the JSON string
public class Customer {
public String firstName;
public String lastName;
public int age;
public String emailAddress;
public Address address;
public PhoneNumber phoneNumber;
public class Address {
public String streetAddress;
public String city;
public String postCode;
public String state;
public String country;
public String county;
}
public class PhoneNumber {
public String home;
public String mob;
}
}
- First just make an extra entry on the Configuration.properties file to specify the JSON file path
testDataResourcePath=src/test/resources/testDataResources/
with above, complete Configuration file will become like this:
environment=local
browser=chrome
windowMaximize=true
driverPath=src/drivers/chromedriver
testDataResourcePath=src/test/resources/testDataResources/
url=http://shop.demoqa.com
implicitWait=10
explicitWait=5
- Create a read method in the Config File Reader class to read JSON file location:
public String getTestDataResourcePath(){
String testDataResourcePath = properties.getProperty("testDataResourcePath");
if(testDataResourcePath!= null) return testDataResourcePath;
else throw new RuntimeException("Test Data Resource Path not specified in the Configuration.properties file for the Key: testDataResourcePath");
}
In the above code, we just get the value saved in the config file for key testDataResourcePath. We throw the exception in case of null value returned from getProperty() method or return the value if it is found not null.
Including above method, the complete Config Reader file will become like this:
package dataProviders;
import enums.DriverType;
import enums.EnvironmentType;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class ConfigFileReader {
private Properties properties;
private final String propertyFilePath= "configs/Configuration.properties";
public ConfigFileReader(){
BufferedReader reader;
try {
reader = new BufferedReader(new FileReader(propertyFilePath));
//Configuration properties can be easily read from .properties file using object of type Properties provided by java.utils
properties = new Properties();
try {
properties.load(reader);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
throw new RuntimeException("Configuration.properties not found at " + propertyFilePath);
}
}
public String getDriverPath(){
String driverPath = properties.getProperty("driverPath");
if(driverPath!= null) return driverPath;
else throw new RuntimeException("driverPath not specified in the Configuration.properties file.");
}
public long getCustomWait(String waitTypeKey) {
String customWait = properties.getProperty(waitTypeKey);
if(customWait != null) return Long.parseLong(customWait);
else throw new RuntimeException("Custom Wait not specified in the Configuration.properties file.");
}
public String getApplicationUrl() {
String url = properties.getProperty("url");
if(url != null) return url;
else throw new RuntimeException("url not specified in the Configuration.properties file.");
}
public DriverType getBrowser() {
String browserName = properties.getProperty("browser");
if(browserName == null || browserName.equals("chrome")) return DriverType.CHROME;
else if(browserName.equalsIgnoreCase("firefox")) return DriverType.FIREFOX;
else throw new RuntimeException("Browser Name Key value in Configuration.properties is not matched : " + browserName);
}
public EnvironmentType getEnvironment() {
String environmentName = properties.getProperty("environment");
if(environmentName == null || environmentName.equalsIgnoreCase("local")) return EnvironmentType.LOCAL;
else if(environmentName.equals("remote")) return EnvironmentType.REMOTE;
else throw new RuntimeException("Environment Type Key value in Configuration.properties is not matched : " + environmentName);
}
public Boolean getBrowserWindowSize() {
String windowSize = properties.getProperty("windowMaximize");
if(windowSize != null) return Boolean.valueOf(windowSize);
return true;
}
public String getTestDataResourcePath(){
String testDataResourcePath = properties.getProperty("testDataResourcePath");
if(testDataResourcePath!= null) return testDataResourcePath;
else throw new RuntimeException("Test Data Resource Path not specified in the Configuration.properties file for the Key: testDataResourcePath");
}
}
GSON is an open source code and it’s used a lot in working with JSON and Java. GSON uses Java Reflection to provide simple methods to convert JSON to java and vice versa. It can be downloaded as GSON jar file from google code website or if used maven, added as a dependency.
- Add GSON Maven dependency to our pom.xml file:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
GSON API also supports out of the box JSON to Java Object conversion if the object field names are same as in JSON. GSON is the main class that exposes the methods fromJson() and toJson() for conversion. For default implementation, we can create this object directly or we can use GsonBuilder class that provide useful options for conversion. 2) Create a new class in src/main/java under 'dataProviders' package and name it 'JsonDataReader'
package dataProviders;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import com.google.gson.Gson;
import managers.FileReaderManager;
import testDataTypes.Customer;
public class JsonDataReader {
private final String customerFilePath = FileReaderManager.getInstance().getConfigReader().getTestDataResourcePath() + "Customer.json";
private List<Customer> customerList;
public JsonDataReader(){
customerList = getCustomerData();
}
private List<Customer> getCustomerData() {
Gson gson = new Gson();
BufferedReader bufferReader = null;
try {
bufferReader = new BufferedReader(new FileReader(customerFilePath));
Customer[] customers = gson.fromJson(bufferReader, Customer[].class);
return Arrays.asList(customers);
}catch(FileNotFoundException e) {
throw new RuntimeException("Json file not found at path : " + customerFilePath);
}finally {
try { if(bufferReader != null) bufferReader.close();}
catch (IOException ignore) {}
}
}
public final Customer getCustomerByName(String customerName){
for(Customer customer : customerList) {
if(customer.firstName.equalsIgnoreCase(customerName)) return customer;
}
return null;
}
}
getCustomerData() : This is a private method, which has the logic implemented to read the Customer Json and save it to the class instance variable.
JsonDataReader() : Here the responsibility of the constructor is to call getCustomerData() method only.
getCustomerByName() : This just filter method. It scans all the information and return the specific customer to the test.
As we have a FileReaderManager singleton class over all the readers, so we need to make an entry of JsonDataReader in that as well.
package managers;
import dataProviders.ConfigFileReader;
import dataProviders.JsonDataReader;
public class FileReaderManager {
private static FileReaderManager fileReaderManager = new FileReaderManager();
private static ConfigFileReader configFileReader;
private static JsonDataReader jsonDataReader;
private FileReaderManager() {
}
public static FileReaderManager getInstance( ) {
return fileReaderManager;
}
public ConfigFileReader getConfigReader() {
return (configFileReader == null) ? new ConfigFileReader() : configFileReader;
}
public JsonDataReader getJsonReader(){
return (jsonDataReader == null) ? new JsonDataReader() : jsonDataReader;
}
}
All the setup work is done, it is time to move closer to the test. First, we need to create a new method inside our CheckoutPage called CustomerPersonalDetailsFromJSON. This method will take customer data stored in Customer object and enter it into checkout form.
public void CustomerPersonalDetailsFromJSON(Customer customer, long customTimeout) throws InterruptedException {
if(wait.WaitForVisibleWithCustomTimeout(driver,txtbx_FirstName, customTimeout)) {
txtbx_FirstName.sendKeys(customer.firstName);
txtbx_LastName.sendKeys(customer.lastName);
select_Country(customer.address.country);
txtbx_City.sendKeys(customer.address.city);
txtbx_Address.sendKeys(customer.address.streetAddress);
txtbx_PostCode.sendKeys(customer.address.postCode);
Thread.sleep(2000);
txtbx_Phone.sendKeys(customer.phoneNumber.mob);
txtbx_Email.sendKeys(customer.emailAddress);
}
}
As we already have modified our feature file in the first step, now we need to make necessary changes to the step file as well. We need to add a new step which comes from Scenario Outline so we run our test first to get a code snippet for it.
- Run TestRunner and copy a code snippet from the console window to the CheckoutPageSteps definition file.
@When("I enter (.+) personal details")
public void i_enter_personal_details() {
// Write code here that turns the phrase above into concrete actions
throw new cucumber.api.PendingException();
}
- Add the following code inside this new step:
@When("I enter (.+) personal details")
public void i_enter_personal_details(String customerName) throws InterruptedException {
Customer customer = FileReaderManager.getInstance().getJsonReader().getCustomerByName(customerName);
checkoutPage.CustomerPersonalDetailsFromJSON(customer, customTimeout);
}
Fetching the Customer data from json reader using getCustomerByName() by passing the Customer Name. Supplying the same data to the Checkout page objects CustomerPersonalDetailsFromJSON() method. The complete class should look like this:
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.java.en.When;
import managers.FileReaderManager;
import pageObjects.CheckoutPage;
import testDataTypes.Customer;
import testDataTypes.CustomerDataType;
import java.util.List;
public class CheckoutPageSteps {
TestContext testContext;
CheckoutPage checkoutPage;
long customTimeout = FileReaderManager.getInstance().getConfigReader().getCustomWait("explicitWait");
public CheckoutPageSteps(TestContext context) {
testContext = context;
checkoutPage = testContext.getPageObjectManager().getCheckoutPage();
}
@When("I enter my personal details as follows")
public void i_enter_my_personal_details_as_follows(List<CustomerDataType> inputs) {
checkoutPage.CustomerPersonalDetailsFromDataTable(inputs, customTimeout);
}
@When("I enter (.+) personal details")
public void i_enter_personal_details(String customerName) throws InterruptedException {
Customer customer = FileReaderManager.getInstance().getJsonReader().getCustomerByName(customerName);
checkoutPage.CustomerPersonalDetailsFromJSON(customer, customTimeout);
}
@When("I place the order")
public void i_place_the_order() {
checkoutPage.check_TermsAndCondition(customTimeout);
checkoutPage.clickOn_PlaceOrder(customTimeout);
}
}
Now we have 3 tests to execute (one as a scenario and other two as scenario outline with two different sets of customer data). To save a bit of time when testing newly implemented Data Driven Test using JSON code, we can make a good use of Cucumber tags. Tags are used to filter the test we want to execute at a particular run. In order to do so, we need to make two small changes to feature file and our test runner class:
- Inside our Feature file add @wip tag above Scenario Outline. (wip stands for 'Work In Progress' and is used while test automation development is in progress)
@wip
Scenario Outline: Customer place an order by purchasing an item from search - customer details are taken from JSON file
And I enter <customer> personal details
And I place the order
Then Order details are successfully verified
Examples:
| customer |
| Opencast |
| Testuser |
- Add tag property to @CucumberOptions inside our Test runner class, so only scenario with @wip tag will be executed:
package runners;
import org.junit.runner.RunWith;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
glue= {"stepDefinitions"},
tags={"@wip"}
)
public class TestRunner {
}
Run test with Test Runner Class. Two tests with different sets of customer data should be executed successfully.
When ever we do test execution, it is also require to understand the output of the execution.
When we executing Cucumber Scenarios, it automatically generates an output in the IDE console. There is a default behavior associated with that output and we can also configure that output as per our needs. So let's see how we can modify the default behavior.
The first plugin, we will talk about is Pretty. It provides more verbose output. To implement this, just specify plugin = “pretty” in CucumberOptions.
@CucumberOptions( plugin = { “pretty” } )
If the monochrome option is set to false, then the console output is not as readable as it should be. It is just because, if the monochrome is not defined in Cucumber Options, it sets it to false by default. How to specify it:
@CucumberOptions( monochrome = true );
With new 'Pretty" plugin and monochrome option set to true, CucumberOption code should look like this:
package runners;
import org.junit.runner.RunWith;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
glue= {"stepDefinitions"},
tags={"@wip"},
plugin = { "pretty" },
monochrome = true
)
public class TestRunner {
}
Console output will now look like this:
Feature: Automated End2End Tests
Description: The purpose of this feature is to test End 2 End integration.
@wip
Scenario Outline: Customer place an order by purchasing an item from search - customer details are taken from JSON file # src/test/resources/features/End2End_Test.feature:17
And I enter <customer> personal details
And I place the order
Then Order details are successfully verified
Examples:
Background: # src/test/resources/features/End2End_Test.feature:4
Given I am on Home Page # HomePageSteps.i_am_on_Home_Page()
When I search for product in dress category # HomePageSteps.i_search_for_product_in_dress_category()
And I choose to buy the first item # ProductPageSteps.i_choose_to_buy_the_first_item()
And I move to checkout from mini cart # CartPageSteps.i_move_to_checkout_from_mini_cart()
If we are more concerned about the time taken by each Step Definition, then we should use the usage plugin. This is how we specify the same in @CucumberOptions:
@CucumberOptions( plugin = { “usage” })
Console output will now look like this:
[
{
"source": "I choose to buy the first item",
"steps": [
{
"name": "I choose to buy the first item",
"aggregatedDurations": {
"average": 5.699256593,
"median": 5.699256593
},
"durations": [
{
"duration": 7.602074066,
"location": "src/test/resources/features/End2End_Test.feature:7"
},
{
"duration": 3.79643912,
"location": "src/test/resources/features/End2End_Test.feature:7"
}
]
}
]
},
{
"source": "I place the order",
"steps": [
{
"name": "I place the order",
"aggregatedDurations": {
"average": 0.392438454,
"median": 0.392438454
},
"durations": [
{
"duration": 0.376548105,
"location": "src/test/resources/features/End2End_Test.feature:19"
},
{
"duration": 0.408328804,
"location": "src/test/resources/features/End2End_Test.feature:19"
}
]
}
]
} ...
So far what we have seen above is actually good for a test or for couple of tests. But if we run a full test suite, this report becomes not very useful. On top of that it is difficult to keep these console output safe for future use.
Cucumber gives us capability to generate reports in the form of HTML, XML, JSON & TXT. Cucumber framework generate very good and detailed reports, which can be shared with all stakeholders. There are multiple options available for reports which can be used depending on the business requirements.
For HTML reports, add html:target/cucumber-reports to the @CucumberOptions plugin option.
@CucumberOptions(
features = "src/test/resources/features",
glue= {"stepDefinitions"},
tags={"@wip"},
plugin = { "pretty", "html:target/cucumber-reports" }
)
Note: We have specified the path for the Cucumber report, which we want to generate under the 'target' folder.
Run Test Runner and check HTML report output inside target/cucumber-reports.
For JSON reports, add json:target/cucumber-reports/Cucumber.json to the @CucumberOptions plugin option.
@CucumberOptions(
features = "src/test/resources/features",
glue= {"stepDefinitions"},
tags={"@wip"},
plugin = { "pretty"
, "html:target/cucumber-reports"
, "json:target/cucumber-reports/Cucumber.json"}
)
Note : This report contains all the information from the gherkin source in the JSON format. This report is meant to be post processed into another visual format by third-party tools. JSON report output will look like this:
[
{
"line": 1,
"elements": [
{
"line": 4,
"name": "",
"description": "",
"type": "background",
"keyword": "Background",
"steps": [
{
"result": {
"duration": 4848418704,
"status": "passed"
},
"line": 5,
"name": "I am on Home Page",
"match": {
"location": "HomePageSteps.i_am_on_Home_Page()"
},
"keyword": "Given "
},
{
"result": {
"duration": 3078654424,
"status": "passed"
}...
For JUNIT reports, add junit:target/cucumber-reports/Cucumber.xml to the @CucumberOptions plugin option.
@CucumberOptions(
features = "src/test/resources/features",
glue= {"stepDefinitions"},
tags={"@wip"},
plugin = { "pretty"
, "html:target/cucumber-reports"
, "json:target/cucumber-reports/Cucumber.json"
, "junit:target/cucumber-reports/Cucumber.xml"}
)
Note : This report generates XML file just like Apache Ant’s junit report task. This XML format is understood by most continuous integration servers, who will use it to generate visual reports. XML Report Output:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<testsuite failures="0" name="cucumber.runtime.formatter.JUnitFormatter" skipped="0" tests="2" time="57.044076">
<testcase classname="Automated End2End Tests" name="Customer place an order by purchasing an item from search - customer details are taken from JSON file" time="28.534119">
<system-out><![CDATA[Given I am on Home Page.....................................................passed
When I search for product in dress category.................................passed
And I choose to buy the first item..........................................passed
And I move to checkout from mini cart.......................................passed
And I enter Opencast personal details.......................................passed
And I place the order.......................................................passed
Then Order details are successfully verified................................passed
]]></system-out>
</testcase>
<testcase classname="Automated End2End Tests" name="Customer place an order by purchasing an item from search - customer details are taken from JSON file 2" time="28.509957">
<system-out><![CDATA[Given I am on Home Page.....................................................passed
When I search for product in dress category.................................passed
And I choose to buy the first item..........................................passed
And I move to checkout from mini cart.......................................passed
And I enter Testuser personal details.......................................passed
And I place the order.......................................................passed
Then Order details are successfully verified................................passed
]]></system-out>
</testcase>
</testsuite>
In the last section of Cucumber Reports we got to know about the Plugins which are provided by Cucumber itself to generate various kind of reports like HTML, JSON, XML, etc. Generally, those reports are enough to give us the overall execution results with detailed time logs and other things. But there are many third party plugins also available which help to produce reports with improved test logs capacity and better visual graphics.
One of such plugins is 'Extent Report' by Anshoo Arora. This is currently considered one of the best reporting plugin on the market. This report fits fine with any test framework you use. With Cucumber as well it works fine but it requires to have some hacks to produce reports. It would have been easy if Cucumber also had annotations like @beforeScenario and @beforeFeature. These annotations are available in SpecFLow which is Cucumber in C#. But there is an alternative for Cucumber called 'Cucumber Extent Report'.
This plugin is build on Extent Report specifically for Cucumber by Vimal Selvam. This is why it is named Cucumber Extent Report. This one is actually made to ease the implementation of Extent Report in Cucumber Framework. Let’s start with implementing the same in our Selenium Cucumber Framework.
The latest version of the extentreports-cucumber4-adapter jar is 1.0.7 which will be primarily used here.
The extentreports-cucumber4-adapter version 1.0.7 jar from maven repository works by default for the following Cucumber versions 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.1.1, 4.2.0.
The extentreports-cucumber4-adapter version 1.0.7 jar from maven repository does not work by default for Cucumber versions 4.2.1 - 4.7.0.
The reason is that the adapter requires the class URLOutputStream, which was moved to cucumber.runtime.formatter package and access changed to package protected in version 4.2.1. In the previous version it was a public class in cucumber.runtime.io package. Though this class exists in the github source of extentreports-cucumber4-adapter, it has not been included in the jar. Therefore, the extentreports-cucumber4-adapter jar needs to be modified for running with Cucumber version greater than 4.2.0
As we are using cucumber version 4.2.0, so extentreports-cucumber4-adapter will work for us by default, however, we need to be mindful if/when upgrading cucumber to a higher version as it may break our reporting functionality if extentreports-cucumber4-adapter modification is not addressed as a part of the upgrade process.
- Add Extent Report Adapter to pom.xml
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports-cucumber4-adapter</artifactId>
<version>1.0.7</version>
</dependency>
- Add Extent Report library
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>4.0.9</version>
</dependency>
Extent Config is required by the Cucumber Extent Report plugin to read the report configuration. It gives us the capability to set many useful setting to the report from the XML configuration file. By default, it is read from the resources folder in scr/test so we need to place our Extent Report Configuration file into this specific location.
- Create a New File in src/test/ inside resources folder and name it 'extent-config.xml'
<?xml version="1.0" encoding="UTF-8"?>
<extentreports>
<configuration>
<!-- report theme -->
<!-- standard, dark -->
<theme>dark</theme>
<!-- document encoding -->
<!-- defaults to UTF-8 -->
<encoding>UTF-8</encoding>
<!-- protocol for script and stylesheets -->
<!-- defaults to https -->
<protocol>https</protocol>
<!-- title of the document -->
<documentTitle>Automation Reports</documentTitle>
<!-- report name - displayed at top-nav -->
<reportName>Automation Report for Cucumber Framework Tutorial</reportName>
<!-- report headline - displayed at top-nav, after reportHeadline -->
<reportHeadline>Behaviour Driven Framework</reportHeadline>
<!-- global date format override -->
<!-- defaults to yyyy-MM-dd -->
<dateFormat>yyyy-MM-dd</dateFormat>
<!-- global time format override -->
<!-- defaults to HH:mm:ss -->
<timeFormat>HH:mm:ss</timeFormat>
<!-- custom javascript -->
<scripts>
<![CDATA[
$(document).ready(function() {
});
]]>
</scripts>
<!-- custom styles -->
<styles>
<![CDATA[
]]>
</styles>
</configuration>
</extentreports>
First step of activating the report generation is to place extent.properties file in the src/test/resources folder to be picked up by the adapter.
- Create a New File in src/test/ inside resources folder and name it 'extent.properties' Note: We will only be activating HTML Report in this tutorial:
- set flag to true for extent.reporter.html.start key
- specify the location for the report file: html report needs to be mentioned as a value for the key extent.reporter.html.config
- specify the location for the report path for extent.reporter.html.out key
extent.reporter.avent.start=false
extent.reporter.bdd.start=false
extent.reporter.cards.start=false
extent.reporter.email.start=false
extent.reporter.html.start=true
extent.reporter.klov.start=false
extent.reporter.logger.start=false
extent.reporter.tabular.start=false
extent.reporter.avent.config=
extent.reporter.bdd.config=
extent.reporter.cards.config=
extent.reporter.email.config=
extent.reporter.html.config=src/test/resources/ReportsConfig.xml
extent.reporter.klov.config=
extent.reporter.logger.config=
extent.reporter.tabular.config=
extent.reporter.avent.out=
extent.reporter.bdd.out=
extent.reporter.cards.out=
extent.reporter.email.out=
extent.reporter.html.out=test-output/HtmlReport/ExtentHtml.html
extent.reporter.logger.out=
extent.reporter.tabular.out=
- Add com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter: plugin to @CucumberOptions inside our Test Runner
package runners;
import org.junit.runner.RunWith;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
glue= {"stepDefinitions"},
tags={"@wip"},
plugin = { "pretty"
, "html:target/cucumber-reports"
, "json:target/cucumber-reports/Cucumber.json"
, "junit:target/cucumber-reports/Cucumber.xml"
, "com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:"}
)
public class TestRunner {
}
The above setup will generate the report in test-output/HtmlReport/ directory with the name of ExtentHtml.html. Run Test Runner and check that the report has been generated.
As it was mentioned above, Cucumber Extent Report provides nice features to make a report very useful. We will look at them one by one.
This gives us a nice feature to set multiple System properties to the report, so that we know under which system configurations our test suite was executed, when, and by whom. In order to implement this, we do the following:
- Create a new Class in src/main/java under utils package and name it 'ExtentReportBuilder'
- Inside ExtentReportBuilder class we will make use of WebDriver's getCapabilities()method. I provides information regarding the current instance of a driver. We will use it for getting browser name and version. We also make use of System.getProperty() method to get information about Platform and OC version alongside with the details of a user who executed the test. Extent Report Builder Class should look like this:
package utils;
import cucumber.TestContext;
import cucumber.api.Scenario;
import managers.WebDriverManager;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
public class ExtentReportBuilder {
TestContext testContext;
WebDriverManager driverManager;
public ExtentReportBuilder (TestContext context) {
testContext = context;
driverManager = testContext.getWebDriverManager();
}
public void additionalReportInfo(Scenario scenario) {
Capabilities cap = ((RemoteWebDriver) driverManager.getDriver()).getCapabilities();
String browserName = cap.getBrowserName().toUpperCase();
String browserVersion = cap.getVersion();
scenario.write("Executed by: " + System.getProperty("user.name"));
scenario.write("Platform: " + System.getProperty("os.name") + " (" + System.getProperty("os.arch") + ") v." + System.getProperty("os.version"));
scenario.write("Browser: " + browserName + " v. " + browserVersion);
}
}
- Invoke additionalReportInfo() method from @After Hook like so:
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.Scenario;
import cucumber.api.java.After;
import cucumber.api.java.Before;
import utils.ExtentReportBuilder;
public class Hooks {
TestContext testContext;
ExtentReportBuilder reportBuilder;
public Hooks(TestContext context) {
testContext = context;
reportBuilder = new ExtentReportBuilder(context);
}
@Before
public void BeforeSteps() {
testContext.getWebDriverManager().getDriver();
}
@After
public void AfterSteps(Scenario scenario) {
try {
reportBuilder.additionalReportInfo(scenario);
testContext.getWebDriverManager().closeDriver();
} catch (Exception e) {
System.out.println("Methods failed: tearDownAndScreenShotOnFailure, Exception:" + e.getMessage());
}
}
}
Run Test Runner and check ExtentHtml.html by opening it in a browser. It should now have System Information in it inside 'Hooks.AfterStep' step;
Hooks.AfterSteps(Scenario)
Executed by: testowner
Platform: Mac OS X (x86_64) v.10.14.3
Browser: CHROME v. 75.0.3770.142
This feature gives us the capability to embed a screenshot in the report. Below we will step by step capture a screenshot and attach it to the report, only if the scenario is Failed. In case of Pass, no screenshot will be taken.
- Add 'commons-io' library to our pom.xml file
The Apache Commons-io library contains utility classes, stream implementations, file filters and other features related to file manipulations.
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
- Add a new variable to src/test/resources/extent.properties file: a path where we save our screenshots:
saveScreenshotsTo = test-output/Screenshots/
-
Create a new empty folder inside test-output folder and name it 'Screenshots'
-
Create a new config reader in src/main/java under 'dataProviders' package and name it 'ExtentReportConfigReader'. It will read reports related data from extent.properties file
package dataProviders;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class ExtentReportConfigReader {
private Properties properties;
private final String propertyFilePath= "src/test/resources/extent.properties";
public ExtentReportConfigReader(){
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(propertyFilePath));
properties = new Properties();
try { properties.load(reader); }
catch (IOException e) { e.printStackTrace(); }
} catch (FileNotFoundException e) {
throw new RuntimeException("Properties file not found at path : " + propertyFilePath);
}finally {
try { if(reader != null) reader.close(); }
catch (IOException ignore) {}
}
}
public String getSaveScreenShotsTo(){
String saveScreenShotsTo = properties.getProperty("saveScreenshotsTo");
if(saveScreenShotsTo!= null) return saveScreenShotsTo;
else throw new RuntimeException("Screenshots path not specified in the Configuration.properties file for the Key: saveScreenshotsTo");
}
}
- Add ExtentReportConfigReader to FileReader Manager class
package managers;
import dataProviders.ConfigFileReader;
import dataProviders.ExtentReportConfigReader;
import dataProviders.JsonDataReader;
public class FileReaderManager {
private static FileReaderManager fileReaderManager = new FileReaderManager();
private static ConfigFileReader configFileReader;
private static JsonDataReader jsonDataReader;
private static ExtentReportConfigReader extentReportConfigReader;
private FileReaderManager() {
}
public static FileReaderManager getInstance( ) {
return fileReaderManager;
}
public ConfigFileReader getConfigReader() {
return (configFileReader == null) ? new ConfigFileReader() : configFileReader;
}
public JsonDataReader getJsonReader(){
return (jsonDataReader == null) ? new JsonDataReader() : jsonDataReader;
}
public ExtentReportConfigReader getExtentReportConfigReader() {
return (extentReportConfigReader == null) ? new ExtentReportConfigReader() : extentReportConfigReader;
}
}
- Add methods to ExtentReportBuilder Class which will capture a screenshot for a failing scenario
package utils;
import cucumber.TestContext;
import cucumber.api.Scenario;
import managers.FileReaderManager;
import managers.WebDriverManager;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.apache.commons.io.FileUtils;
import java.io.*;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class ExtentReportBuilder {
TestContext testContext;
WebDriverManager driverManager;
private String screenshotName;
private String saveScreenshotsTo;
private String archivedReportName;
private String saveArchiveReportsTo;
private String latestReportPath;
public ExtentReportBuilder (TestContext context) {
testContext = context;
driverManager = testContext.getWebDriverManager();
saveScreenshotsTo = FileReaderManager.getInstance().getExtentReportConfigReader().getSaveScreenShotsTo();
saveArchiveReportsTo = FileReaderManager.getInstance().getExtentReportConfigReader().getSaveArchiveReportsTo();
latestReportPath = FileReaderManager.getInstance().getExtentReportConfigReader().getCurrentReportPath();
}
public void additionalReportInfo(Scenario scenario) {
Capabilities cap = ((RemoteWebDriver) driverManager.getDriver()).getCapabilities();
String browserName = cap.getBrowserName().toUpperCase();
String browserVersion = cap.getVersion();
scenario.write("Executed by: " + System.getProperty("user.name"));
scenario.write("Platform: " + System.getProperty("os.name") + " (" + System.getProperty("os.arch") + ") v." + System.getProperty("os.version"));
scenario.write("Browser: " + browserName + " v. " + browserVersion);
}
public String dateAdjustment(int number) {
return (number < 10) ? ("0" + number) : Integer.toString(number);
}
public String returnDateStamp() {
Date d = new Date();
Calendar calendar = new GregorianCalendar();
calendar.setTime(d);
int yr = calendar.get(Calendar.YEAR);
String year = dateAdjustment(yr);
int mo = calendar.get(Calendar.MONTH) + 1;
String month = dateAdjustment(mo);
int dt = calendar.get(Calendar.DATE);
String dates = dateAdjustment(dt);
int hr = calendar.get(Calendar.HOUR_OF_DAY);
String hours = dateAdjustment(hr);
int mn = calendar.get(Calendar.MINUTE);
String min = dateAdjustment(mn);
int sc = calendar.get(Calendar.SECOND);
String sec = dateAdjustment(sc);
String date = (dates + "_" + month + "_" + year + "_" + hours + "_" + min + "_" + sec);
return date;
}
public String returnScreenshotPath() {
return (System.getProperty("user.dir") + "/"+ saveScreenshotsTo + screenshotName);
}
public void captureScreenshot(Scenario scenario) throws IOException {
if (scenario.isFailed()) {
File srcFile = ((TakesScreenshot) driverManager.getDriver()).getScreenshotAs(OutputType.FILE);
screenshotName = returnDateStamp() + ".png";
FileUtils.copyFile(srcFile, new File(returnScreenshotPath()));
scenario.write("<br>");
scenario.write("Taking a screenshot for a failing step");
scenario.write("<a target=\"_blank\", href=" + returnScreenshotPath() + ">" + screenshotName);
scenario.write("<a target=\"_blank\", href=" + returnScreenshotPath() + "><img src=" + returnScreenshotPath() + " height=200 width=300></img></a>");
}
}
}
returnDateStamp() : is a helper method aiding in constructing screenshot filename which is a date stamp returnScreenshotName() : Uses returnScreenshotPath() returns file path where screenshot should be saved captureScreenshot(Scenario scenario) : This method does all the main work - captures screenshot, saves it to a file, constructs file name and assign it to the file and place it into the report
- Invoke captureScreenshot() method from cucumber @after hook
Note: We will give a more meaningful name to our AfterSteps() method. It will be called tearDownAndScreenShotOnFailure()
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.Scenario;
import cucumber.api.java.After;
import cucumber.api.java.Before;
import utils.ExtentReportBuilder;
public class Hooks {
TestContext testContext;
ExtentReportBuilder reportBuilder;
public Hooks(TestContext context) {
testContext = context;
reportBuilder = new ExtentReportBuilder(context);
}
@Before
public void BeforeSteps() {
testContext.getWebDriverManager().getDriver();
}
@After
public void tearDownAndScreenShotOnFailure(Scenario scenario) {
try {
reportBuilder.additionalReportInfo(scenario);
reportBuilder.captureScreenshot(scenario);
testContext.getWebDriverManager().closeDriver();
} catch (Exception e) {
System.out.println("Methods failed: tearDownAndScreenShotOnFailure, Exception:" + e.getMessage());
}
}
}
Now we all set to test our screenshot taken feature. Let's make a change in your test code so we intentionally failing it. (e.g. change one of the @FindBy locators to non-existing). Run the TestRunner and check that screenshot is saved at the specified location and is added to the report.
When dealing with a real life project, business requirement might be to keep track of all previous test execution results. In this case we need to find a way how we preserve a previous report before it is overwritten with a new report data.
To achieve the above, we will add a new variable into our extent.properties file to specify previous reports storage location and write some methods which will be responsible for copying a previous report before its being updated after new test is run. We will make use of our returnDateStamp() method again so we can capture the date, previous report was archived. We will be following the below steps:
- Add a new variable to src/test/resources/extent.properties file: a path where we save our previous reports:
archiveReportsTo = test-output/ArchivedReports/
-
Create a new empty folder inside test-output folder and name it 'ArchivedReports'
-
Add two methods to our ExtentReportConfigReader Class which will read current report path from "extent.reporter.html.out" key and a path for previous report to be stored;
package dataProviders;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class ExtentReportConfigReader {
private Properties properties;
private final String propertyFilePath= "src/test/resources/extent.properties";
public ExtentReportConfigReader(){
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(propertyFilePath));
properties = new Properties();
try { properties.load(reader); }
catch (IOException e) { e.printStackTrace(); }
} catch (FileNotFoundException e) {
throw new RuntimeException("Properties file not found at path : " + propertyFilePath);
}finally {
try { if(reader != null) reader.close(); }
catch (IOException ignore) {}
}
}
public String getSaveScreenShotsTo(){
String saveScreenShotsTo = properties.getProperty("saveScreenshotsTo");
if(saveScreenShotsTo!= null) return saveScreenShotsTo;
else throw new RuntimeException("Screenshots path not specified in the Configuration.properties file for the Key: saveScreenshotsTo");
}
public String getSaveArchiveReportsTo(){
String archiveReportsTo = properties.getProperty("archiveReportsTo");
if(archiveReportsTo!= null) return archiveReportsTo;
else throw new RuntimeException("Archived Reports path not specified in the Configuration.properties file for the Key: archiveReportsTo");
}
public String getCurrentReportPath(){
String latestReportPath = properties.getProperty("extent.reporter.html.out");
if(latestReportPath!= null) return latestReportPath;
else throw new RuntimeException("Latest Reports path not specified in the Configuration.properties file for the Key: extent.reporter.html.out");
}
}
- Add two new methods to ExtentReportBuilder Class for renaming and copying previous report file:
public void copyFileUsingStream(File source, File dest) throws IOException {
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(source);
os = new FileOutputStream(dest);
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}catch (Exception e) {
System.out.println("Unable to copy the report" + e.getMessage());
} finally {
is.close();
os.close();
}
}
public void copyLatestExtentReport() throws IOException {
archivedReportName = "ArchivedOn_" + returnDateStamp(".html");
File source = new File(System.getProperty("user.dir") +"/"+ latestReportPath);
File dest = new File(System.getProperty("user.dir") +"/"+ saveArchiveReportsTo + archivedReportName);
copyFileUsingStream(source, dest);
}
copyFileUsingStream() : This is a standard Java method for copying files copyLatestExtentReport() : This method specifies what file needs to be copied and where to. We will invoke this method from our @after Hook in Hooks Class
- Invoke copyLatestExtentReport() method from cucumber @after hook
package stepDefinitions;
import cucumber.TestContext;
import cucumber.api.Scenario;
import cucumber.api.java.After;
import cucumber.api.java.Before;
import utils.ExtentReportBuilder;
public class Hooks {
TestContext testContext;
ExtentReportBuilder reportBuilder;
public Hooks(TestContext context) {
testContext = context;
reportBuilder = new ExtentReportBuilder(context);
}
@Before
public void BeforeSteps() {
testContext.getWebDriverManager().getDriver();
}
@After
public void tearDownAndScreenShotOnFailure(Scenario scenario) {
try {
reportBuilder.additionalReportInfo(scenario);
reportBuilder.captureScreenshot(scenario);
reportBuilder.copyLatestExtentReport();
testContext.getWebDriverManager().closeDriver();
} catch (Exception e) {
System.out.println("Methods failed: tearDownAndScreenShotOnFailure, Exception:" + e.getMessage());
}
}
}
Run the Test Runner and check that the previous report is now stored in ArchivedReport folder. Run test again and check that another previous report is added too.
=======
860f3ad8ddf1cae3389d29ca597402813896e927