Sunday 11 June 2023

Cucumber scenarios parallel execution with ThreadLocal driver and dynamic dataproviderthreadcount | TestNG, Java Selenium, Maven , POM page factory

Hi, In this post, we will see how to run cucumber scenarios in parallel with TestNG.

What is parallel testing in cucumber?
Parallel testing in Cucumber refers to the ability to execute Cucumber scenarios in parallel, allowing multiple scenarios to run simultaneously and speeding up the overall test execution time.

Consider below factors while implementing parallel execution. 
  • Scenario Independence
  • Thread Safety
  • Tagging Scenarios
  • Synchronization

Software :
Java :  "17.0.7" 2023-04-18 LTS  (java -version)
Maven : Apache Maven 3.9.2  (mvn -version)
Chrome browser: 114.0.5735.110 (Official Build) (64-bit)
Chrome WebDriver: 114.0.5735.90
https://chromedriver.storage.googleapis.com/index.html?path=114.0.5735.90/

cucumber-java 7.12.0
cucumber-testng 7.12.0
selenium-java 4.10.0
maven-surefire-plugin 3.1.2
maven-compiler-plugin 3.11.0

Example: Execute two scenarios in parallel from a single feature file.
i.e.,
1) Verify user is able to login to the application with valid credentials
2) Verify Forgot password link is displayed on login page.

Is it necessary to have testng.xml for parallel execution ?
There could be different approaches to perform parallel execution from command line or jar file or from CI/CD method, in this demo I'd say yes it is necessary to have testng.xml so that we can pass dynamic thread count. 

What is ThreadLocal webdriver in cucumber ? Why it is important ?
A ThreadLocal webdriver refers to an instance of a WebDriver object that is stored in a ThreadLocal variable. 
Each thread running a Cucumber scenario or step can have its own WebDriver instance, isolated from other threads.
This approach ensures thread safety and prevents conflicts when executing scenarios in parallel.

Here's why ThreadLocal webdriver is important
  • Thread Safety
  • Parallel Execution
  • Resource Management
  • Scenario Isolation
The project folder structure: 
Is mandatory to keep all the code in "parallel" folder as in many example over the internet ? 

No, it is not necessary to keep them in "parallel" folder. Similarly to keep the features NO "parallel" folder is required. 

Concepts covered in code base:
  • DriverFactory for chrome and edge - How to pass chrome or edge as command line argument ? 
  • Parallel execution configurations - Where and how to configure parallel execution with TestNG? 
  • POM model with page factory pattern - How to isolate locators and step definitions ? 
  • Cucumber hooks  - What is the recommendation of cucumber hooks over TestNG hooks ?  
  • Reports 
    • Which report is to be used - cucumber reports or extent reports ?  cucumber reports 
    • Where to write email code to send the latest reports ? In JVM shutdown hook that is written in cucumber AfterAll hook
      (Did you ever try to remove or refresh a folder to have the latest reports to be sent over email)
  • How to run the project ?
    • Through command line 
    • Through eclipse run as TestNG
    • Through executable jar file
Would you like to watch few mins demo in a no voice video ? 

Jumping on to the core of this article i.e., parallel testing 

Parallel execution configurations:

(For complete code download project from GitHub or Use this link to download as Zip

  • Declare thread local driver in WebDriverFactory java class and then set, get and remove instances of the driver. 

    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();

  • Override the DataProvider annotation with parallel=true in CucumberRunnerTest that extends AbstractTestNGCucumberTests class. 

    public class CucumberRunnerTest extends AbstractTestNGCucumberTests {


    @Override

    @DataProvider(parallel = true)

    public Object[][] scenarios() {

    return super.scenarios();

    }

    }

  • Create a testng.xml file and give parallel=true and data-provider-thread-count=1 and the runner class as shown below.

    <?xml version="1.0" encoding="UTF-8"?>

    <!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">

    <suite name="Suite" parallel="true" data-provider-thread-count="1" verbose="2">

    <test name="Test">

    <classes>

    <class name="runners.CucumberRunnerTest" />

    </classes>

    </test> <!--

    Test -->

    </suite> <!--

    Suite -->

  • In pom.xml add the testng.xml as suiteXml file  in maven-surefire-plugin.

    <plugin>

    <groupId>org.apache.maven.plugins</groupId>

    <artifactId>maven-surefire-plugin</artifactId>

    <version>3.1.2</version>

    <configuration>

    <suiteXmlFiles>

    <suiteXmlFile>testng.xml</suiteXmlFile>

    </suiteXmlFiles>

    </configuration>

    </plugin>

That's all we need to make cucumber scenarios to run parallel with TestNG as testing framework. 
So, after configurations - How to run the code to see parallel execution ? 

On the command prompt - maven execution
  • Navigate to the the project folder location - you should have the pom.xml in it. 
  • Run below command in Command prompt. 

    mvn -Dbrowser=chrome -Ddataproviderthreadcount=2 test

  • This command opens two chrome browser parallelly as passing dataproviderthreadcount =2 
  • If passed 1 then sequential execution takes place. 
On the command prompt - jar execution 
  • Use the maven-assembly-plugin and Main.run method creation technique to create a jar file
    (scroll down to the complete code section below and see how assembly plugin is configured in pom.xml and how Main.main takes arguments in CucumberRunnerCLI.java class )

  • Now to create executable/runnable jar file give below goal in eclipse 
    (Runs As -> Maven build -> Goals)  (Note: Make sure to clean the project prior assembling)
     
    clean package assembly:single -Dmaven.test.skip=true

  • Running above command downloads required jar files in .m2 repository and creates and copy the jar file in target folder

    [INFO] Building jar: E:\Automation\CucumberParallelExecutionTestNG\ target\CucumberParallelExecutionTestNG-0.0.1-SNAPSHOT-jar-with-dependencies.jar

    [INFO] ------------------------------------------------------------------------

    [INFO] BUILD SUCCESS

  • Use below command to run the scenarios from jar generated in target folder. 

    java -Ddataproviderthreadcount=1 -Dbrowser=chrome -jar CucumberParallelExecutionTestNG-0.0.1-SNAPSHOT-jar-with-dependencies.jar

In the Eclipse Run As TestNG with Run Configurations

  • Run As -> TestNG -> Run Configurations -> VM Arguments 
     --> -Dbrowser=chrome -Ddataproviderthreadcount=2


This is how one could able to run the scenarios in parallel with TestNG as the testing framework. 


Interesting Research Observations: 
Over the internet there are bunch of suggestions with maven sure fire plugin to play with parallel=method, threadCount=3  and incase of maven fail safe plugin to play with dataproviderthreadcount and value in the properties those didn't work for me at least. 

i.e., None of the below configurations worked to run scenarios parallel. 

maven-surefire-plugin with parallel=true and threadCount=3 DID NOT work <plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-surefire-plugin</artifactId>

<version>3.1.2</version>

<configuration>

<parallel>methods</parallel>

<threadCount>3</threadCount>

<perCoreThreadCount>false</perCoreThreadCount>

<useUnlimitedThreads>false</useUnlimitedThreads>

</configuration>

</plugin>

maven-failsafe-plugin with dataproviderthreadcount property with value DID NOT work.

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-failsafe-plugin</artifactId>

<version>2.22.0</version>

<executions>

<execution>

<goals>

<goal>integration-test</goal>

<goal>verify</goal>

</goals>

<configuration>

<properties>

<property>

<name>dataproviderthreadcount</name>

<value>2</value>

</property>

</properties>

</configuration>

</execution>

</executions>

</plugin>

----------------------------------------------------------------------------------------------------------


Code base: 

Login.feature
#Author: Sadakar Pochampalli
@LoginFeature
Feature: Login page validations

  Background: 
    Given User is on login page

  @Login
  Scenario: Login to Orange HRM
    When User enters username "Admin"
    And User enters password "admin123"
    And User clicks on Login button
    Then User should navigate to Orange HRM home page

  @ForgotPassword
  Scenario: Forgot password link verification
    Then User verifies Forgot password link display

  pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>CucumberParallelExecutionTestNG</groupId>
	<artifactId>CucumberParallelExecutionTestNG</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>CucumberParallelExecutionTestNG</name>
	<description>CucumberParallelExecutionTestNG</description>
	<dependencies>
		<dependency>
			<groupId>io.cucumber</groupId>
			<artifactId>cucumber-java</artifactId>
			<version>7.12.0</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-java -->
		<dependency>
			<groupId>io.cucumber</groupId>
			<artifactId>cucumber-testng</artifactId>
			<version>7.12.0</version>
		</dependency>
		<dependency>
			<groupId>org.seleniumhq.selenium</groupId>
			<artifactId>selenium-java</artifactId>
			<version>4.10.0</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.testng/testng -->
		<!--	<dependency>
			<groupId>org.testng</groupId>
		<artifactId>testng</artifactId>
			<version>7.8.0</version>
		</dependency>
-->
		<dependency>
			<groupId>tech.grasshopper</groupId>
			<artifactId>extentreports-cucumber7-adapter</artifactId>
			<version>1.2.0</version>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.24</version>
			<scope>provided</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
		<dependency>
			<groupId>javax.mail</groupId>
			<artifactId>mail</artifactId>
			<version>1.4.7</version>
		</dependency>

	</dependencies>

	<build>
		<pluginManagement>
			<plugins>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-surefire-plugin</artifactId>
					<version>3.1.2</version>
					<configuration>
						<suiteXmlFiles>
							<suiteXmlFile>testng.xml</suiteXmlFile>
						</suiteXmlFiles>
					</configuration>
				</plugin>

				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.11.0</version>
					<configuration>
						<source>17</source>
						<target>17</target>
					</configuration>
				</plugin>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-assembly-plugin</artifactId>
					<version>3.1.1</version>
					<configuration>
						<archive>
							<manifest>
								<addClasspath>true</addClasspath>
								<mainClass>runners.CucumberRunnerCLI</mainClass>
							</manifest>
							<manifestEntries>
								<Class-Path>.</Class-Path>
							</manifestEntries>
						</archive>
						<descriptorRefs>
							<descriptorRef>jar-with-dependencies</descriptorRef>
						</descriptorRefs>
					</configuration>
					<executions>
						<execution>
							<id>make-assembly</id>
							<phase>package</phase>
							<goals>
								<goal>single</goal>
							</goals>
						</execution>
					</executions>
				</plugin>

			</plugins>
		</pluginManagement>
	</build>
</project>

testng.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Suite" parallel="true" data-provider-thread-count="1" verbose="2">
	<test name="Test">
		<classes>
			<class name="runners.CucumberRunnerTest" />
		</classes>
	</test> <!--
	Test -->
</suite> <!--
Suite -->

WebDriverFactory.java
package driverfactory;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;

public final class WebDriverFactory {

	private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();

	public static void setDriver() {

		String browser = System.getProperty("browser", "chrome");
		if (browser.equalsIgnoreCase("chrome")) {
			System.setProperty("webdriver.chrome.driver", "E:\\Drivers\\chromedriver.exe");
			ChromeOptions options = new ChromeOptions();
			options.addArguments("--remote-allow-origins=*");
			driver.set(new ChromeDriver(options));
			getDriver().manage().window().maximize();
			getDriver().manage().deleteAllCookies();
		} else if (browser.equalsIgnoreCase("edge")) {
			System.setProperty("webdriver.edge.driver", "E:\\Drivers\\msedgedriver.exe");
			driver.set(new EdgeDriver());
			getDriver().manage().window().maximize();
			getDriver().manage().deleteAllCookies();
		}
	}

	public static WebDriver getDriver() {
		return driver.get();
	}

	public static void closeDriver() {
		driver.get().quit();
		driver.remove();
	}

}

Hooks.java
package hooks;

import java.net.InetAddress;
import java.net.UnknownHostException;

import driverfactory.WebDriverFactory;
import io.cucumber.java.After;
import io.cucumber.java.AfterAll;
import io.cucumber.java.Before;
import io.cucumber.java.Scenario;

public class Hooks {

	@Before(order = 0)
	public void before(Scenario scenaio) throws Exception {

		WebDriverFactory.setDriver();
		System.out.println("Current Thread Name:" + Thread.currentThread().getName());
		System.out.println("Current Thread ID:" + Thread.currentThread().getId());
	}

	@After(order = 0)
	public void after() throws Exception {
		WebDriverFactory.closeDriver();
	}

	@AfterAll(order = 0)
	public static void afterAll() throws UnknownHostException {
		System.out.println("AfterAll - with order=0");

		InetAddress localhost = InetAddress.getLocalHost();
		System.out.println("System IP Address : " + (localhost.getHostAddress()).trim());

		Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
			@Override
			public void run() {

				System.out.println("Write email code in this method - Shutdown Hook is running and this text prints before JVM shut downs!");
			}
		}));
		System.out.println("This text prints before Shutdown hook");

	}

}

LoginPageFactory.java
package pagefactory;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

public class LoginPageFactory {

	public WebDriver driver;

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

	// Username locator
	@FindBy(xpath = "//input[@name='username']")
	public WebElement userName;

	// Password locator
	@FindBy(xpath = "//input[@name='password']")
	public WebElement passWord;

	// Login button locator
	@FindBy(xpath = "//button[@type='submit']")
	public WebElement loginButton;

	// Forgot password locator
	@FindBy(xpath = "//p[@class='oxd-text oxd-text--p orangehrm-login-forgot-header']")
	public WebElement forgotPassword;
	

	// Method that performs Username action
	public void enterUsername(String uname) {
		userName.sendKeys(uname);
	}

	// Method that performs Password action
	public void enterPassword(String pwd) {
		passWord.sendKeys(pwd);
	}

	// Method that performs Login action
	public void clickLogin() {
		loginButton.click();
	}

	// Method that performs Forgot Password link verification
	public boolean isForgotPasswordLinkPresent() {
		return forgotPassword.isDisplayed();
	}

}

CucumberRunnerCLI.java
package runners;

import io.cucumber.core.cli.*;

public class CucumberRunnerCLI {

	public static void main(String[] args) {

		String threadCount = System.getProperty("dataproviderthreadcount", "1");
		Main.run(new String[] { 
				
				"classpath:scenarios", 
				
				"-g", "driverfactory",
				"-g","hooks",
				"-g","pagefactory",
				"-g","runners",
				"-g","stepdef",
				
				"-p","pretty",
				"-p","json:target/cucumber-reports/cucumber.json",
				"-p","html:target/cucumber-reports/cucumber-report.html",
				
				"-m",
				
				"--threads",threadCount
				

		}, Thread.currentThread().getContextClassLoader());
	}
}

CucumberRunnerTest.java
package runners;

import org.testng.annotations.DataProvider;
import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;

@CucumberOptions(

		features = "classpath:scenarios", 
		tags = "@Login or @ForgotPassword", 
		glue = { "driverfactory", "hooks",
				"pagefactory", "runners", "stepdef" }, 
		plugin = { "pretty","json:target/cucumber-reports/cucumber.json",
						"html:target/cucumber-reports/cucucmber-report.html" }, 
		monochrome = true)

public class CucumberRunnerTest extends AbstractTestNGCucumberTests {

	@Override
	@DataProvider(parallel = true)
	public Object[][] scenarios() {
		return super.scenarios();
	}
}

LoginStepDef.java
package stepdef;

import org.openqa.selenium.support.PageFactory;
import org.testng.Assert;

import driverfactory.WebDriverFactory;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import pagefactory.LoginPageFactory;

public class LoginStepDef {
	LoginPageFactory login = PageFactory.initElements(WebDriverFactory.getDriver(), LoginPageFactory.class);

	@Given("User is on login page")
	public void user_is_on_login_page() throws InterruptedException {

		WebDriverFactory.getDriver().get("https://opensource-demo.orangehrmlive.com/web/index.php/auth/login");
		Thread.sleep(3000);
	}

	@When("User enters username {string}")
	public void user_enters_username(String username) {
		login.enterUsername(username);
	}

	@When("User enters password {string}")
	public void user_enters_password(String password) {
		login.enterPassword(password);
	}

	@When("User clicks on Login button")
	public void user_clicks_on_login_button() throws InterruptedException {
		login.clickLogin();
		Thread.sleep(3000);
	}

	@Then("User should navigate to Orange HRM home page")
	public void user_should_navigate_to_orange_hrm_home_page() {

		String expectedURLToNavigate = "https://opensource-demo.orangehrmlive.com/web/index.php/dashboard/index";
		String actualURLNavigated = WebDriverFactory.getDriver().getCurrentUrl();
		Assert.assertEquals(actualURLNavigated, expectedURLToNavigate);
	}

	@Then("User verifies Forgot password link display")
	public void user_verifies_forgot_password_link_display() {
		Assert.assertTrue(login.isForgotPasswordLinkPresent());
	}

}

cucumber.properties
cucumber.publish.enabled=true

Take another look at the folder structure:

I hope this helped you a bit ! If you liked it do subscribe my YouTube channel for interesting tech updates.  

No comments:

Post a Comment