Simple Cucumber-JVM test with TestNG and Selenium
Published at May 30, 2018
Hi! This is example of using TestNG as test runner to run Cucumber-JVM and Selenium
In this article, I am going to assume that you already have experience with Java IDE as well as Maven
Setting Up Development Environment
Required Tools
- Maven
- Integrated Development Environment (IDE) - i will be using IntelliJ IDEA Community Edition in this article.
- Java JDK (7+)
Setup Basic Maven + Cucumber-JVM Project
- Create new maven project
File —> New —> Project —> Maven
- After Project created, enable auto import Maven dependencies (You can enable it manually by below step)
File > Settings > search and select ‘Maven’ > choose ‘Importing’ > enable ‘Import Maven projects automatically
- In your pom.xml, copy and paste below example pom.xml.
make sure you do not change your existing groupId and artifactId
<?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>com.firdausng</groupId>
<artifactId>cucumberTestNgExample</artifactId>
<version>1.0-SNAPSHOT</version>
<properties> <java.version>1.8</java.version>
<maven.compiler.version>3.7.0</maven.compiler.version>
<cucumber.version>3.0.2</cucumber.version>
<selenium.version>3.12.0</selenium.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-java8 -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<version>${cucumber.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-picocontainer -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>${cucumber.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-testng -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-testng</artifactId>
<version>${cucumber.version}</version>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>1.7.2</version>
</dependency>
</dependencies>
</project> - (Optional) If you have not download Cucumber for Java plugin, please do so.
This plugin is used to provide snippet and test runner for Cucumber-JVM
File > Settings > Plugins > search Cucumber for Java > Install
restart your IDE
Add Basic Tests
Create features folder on this path src/test/resources/feature. We will put all our feature files inside this folder
src/test/resources folder might not be created when you created the project in that case you need to manually created this folder
Create new File inside the features folder and named it as google.feature.
Copy and paste below gherkin steps
Feature: Google Test
This is example of using Cucumber-JVM with TestNG and Selenium
Scenario: search google.com
to verify google search is working
Given I go to google
When I query for "cucumber spring selenium"
And click search
Then google page title should become "cucumber spring selenium - Google Search"
Scenario: check search suggestion
to verify suggestion appeared as the user type the query
Given I go to google
When I query for "cucumber spring selenium"
Then i should see "spring cucumber selenium" as 1 of the suggested search Add Step Definition
to complement the feature file, we need to create the corresponding Step Definition class.
Create package called steps under src/test/java. We will stored all the step definition classes in this package.
Create a new class and named it as GoogleStepDef. Paste below code in this class
package steps;
import cucumber.api.PendingException;
import cucumber.api.java.en.And;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
public class GoogleStepDef {
@Given("^I go to google$")
public void iGoToGoogle() throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
@When("^I query for "([^"]*)"$")
public void iQueryFor(String arg0) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
@And("^click search$")
public void clickSearch() throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
@Then("^google page title should become "([^"]*)"$")
public void googlePageTitleShouldBecome(String arg0) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
@Then("^i should see "([^"]*)" as (\d+) of the suggested search$")
public void iShouldSeeAsOfTheSuggestedSearch(String arg0, int arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
} Do not worry about the PendingException since we going to change it later once we have created Page Object Model.
Add Page Object
Create another package called pageobjects. We will keep all the page object here.
Create a class under this package and named it as BasePage
package pageobjects;
import org.openqa.selenium.WebDriver;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
public abstract class BasePage {
private WebDriver driver;
protected String pageTitle;
private static Logger log = Logger.getLogger(BasePage.class.getName());
public BasePage(WebDriver driver){
this.driver = driver;
this.driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); // 10 seconds Implicit Wait
this.driver.manage().window().maximize();
}
public String getPageTitle(){
return driver.getTitle();
}
public void navigate(String url){driver.get(url); }
public abstract boolean isAt();
} Create another class under this package and named it as GooglePage and extends the BasePage
package pageobjects;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.FindBys;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
import java.util.List;
import java.util.NoSuchElementException;
public class GooglePage extends BasePage {
private String url;
private WebDriver driver;
@FindBy(id = "lst-ib")
private WebElement searchTextBox;
@FindBy(name = "btnK")
private WebElement googleSearchBtn;
@FindBy(id = "resultStats")
private WebElement resultCount;
@FindBys(@FindBy(css = "div .sbqs_c"))
private List<WebElement> suggestions;
private String searchQuery = "";
public String getSearchQuery() {
return searchQuery;
}
private void setSearchQuery(String searchQuery) {
this.searchQuery = searchQuery;
}
public GooglePage(WebDriver driver) {
super(driver);
this.driver = driver;
PageFactory.initElements(driver, this);
this.url = "https://www.google.com";
}
@Override
public boolean isAt() {
return false;
}
public void goTo() {
this.driver.get(this.url);
}
public void enterSearchQuery(String text) {
setSearchQuery(text);
WebDriverWait wait = new WebDriverWait(this.driver, 30);
wait.until(ExpectedConditions.elementToBeClickable(searchTextBox)).clear();
searchTextBox.sendKeys(getSearchQuery());
}
public void clickGoogleSearchBtn() {
searchTextBox.submit();
}
public boolean verifySuggestionExist(String suggestionText) {
Wait wait = new FluentWait(this.driver)
.withTimeout(Duration.ofSeconds(30))
.pollingEvery(Duration.ofSeconds(1))
.ignoring(NoSuchElementException.class).ignoring(StaleElementReferenceException.class);
wait.until(ExpectedConditions.elementToBeClickable(suggestions.get(0)));
suggestions.stream()
.forEach(suggest -> System.out.println("suggestion is " + suggest.getText()));
long suggestCount = suggestions.stream()
.filter(suggest -> suggest.getText().contains(suggestionText))
.count();
if (suggestCount > 0) {
return true;
} else {
return false;
}
}
} Wire Page Object to The Step Definition
Open back GoogleStepDef class and copy paste below code
package steps;
import cucumber.api.java.en.And;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import org.testng.Assert;
import pageobjects.GooglePage;
public class GoogleStepDef {
private GooglePage google;
public GoogleStepDef(GooglePage google){
this.google = google;
}
@Given("^I go to google$")
public void iGoToGoogle() throws Throwable {
google.goTo();
}
@When("^I query for "([^"]*)"$")
public void iQueryFor(String query) throws Throwable {
google.enterSearchQuery(query);
}
@And("^click search$")
public void clickSearch() throws Throwable {
google.clickGoogleSearchBtn();
}
@Then("^google page title should become "([^"]*)"$")
public void googlePageTitleShouldBecome(String pageTitle) throws Throwable {
Assert.assertEquals(google.getSearchQuery() +" - Google Search", pageTitle);
}
@Then("^i should see "([^"]*)" as 1 of the suggested search$")
public void iShouldSeeAsOfTheSuggestedSearch(String suggestion) throws Throwable {
Assert.assertTrue(google.verifySuggestionExist(suggestion));
}
} Support Class to Run Selenium
Cucumber-JVM has a built-in support for dependency injection library such as picocontainer, guice and spring.
We going to utilize dependency injection to inject Page Object Class and Webdriver.
We going to use picocontainer as our dependency injection library because it is the easiest to used.
Create a package name util and create a class named SharedDriver.java. Copy paste code below to the class
package util;
import cucumber.api.Scenario;
import cucumber.api.java8.En;
import io.github.bonigarcia.wdm.ChromeDriverManager;
import io.github.bonigarcia.wdm.FirefoxDriverManager;
import io.github.bonigarcia.wdm.InternetExplorerDriverManager;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.events.EventFiringWebDriver;
import java.net.URL;
/**
* <p>
* Example of a WebDriver implementation that has delegates all methods to a static instance (REAL_DRIVER) that is only
* created once for the duration of the JVM. The REAL_DRIVER is automatically closed when the JVM exits. This makes * scenarios a lot faster since opening and closing a browser for each scenario is pretty slow. * To prevent browser state from leaking between scenarios, cookies are automatically deleted before every scenario. * </p>
* <p>
* A new instance of SharedDriver is created for each Scenario and passed to yor Stepdef classes via Dependency Injection
* </p>
* <p>
* As a bonus, screenshots are embedded into the report for each scenario. (This only works
* if you're also using the HTML formatter). * </p>
* <p>
* A new instance of the SharedDriver is created for each Scenario and then passed to the Step Definition classes'
* constructor. They all receive a reference to the same instance. However, the REAL_DRIVER is the same instance throughout * the life of the JVM. * </p>
*/
public class SharedDriver extends EventFiringWebDriver implements En {
private static final WebDriver REAL_DRIVER;
private static final Thread CLOSE_THREAD = new Thread() {
@Override
public void run() {
REAL_DRIVER.close();
}
};
static {
Runtime.getRuntime().addShutdownHook(CLOSE_THREAD);
try {
REAL_DRIVER = getBrowser();
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new Error(throwable);
}
}
public SharedDriver() {
super(REAL_DRIVER);
Before(manage()::deleteAllCookies);
After((Scenario scenario)->{
try {
byte[] screenshot = getScreenshotAs(OutputType.BYTES);
scenario.embed(screenshot, "image/png");
} catch (WebDriverException somePlatformsDontSupportScreenshots) {
System.err.println(somePlatformsDontSupportScreenshots.getMessage());
}
});
}
@Override
public void close() {
if (Thread.currentThread() != CLOSE_THREAD) {
throw new UnsupportedOperationException("You shouldn't close this WebDriver. It's shared and will close when the JVM exits.");
}
super.close();
}
private static WebDriver getBrowser() throws Throwable {
String desiredBrowserName = System.getProperty("browser", "chrome");
WebDriver desiredBrowser = null;
switch(desiredBrowserName) {
case "ie":
InternetExplorerDriverManager.getInstance().setup();
desiredBrowser = new InternetExplorerDriver();
break;
case "chrome":
ChromeDriverManager.getInstance().setup();
desiredBrowser = new ChromeDriver();
break;
case "firefox":
FirefoxDriverManager.getInstance().setup();
desiredBrowser = new FirefoxDriver();
break;
default:
//work out what to do when a browser isn't needed
break;
}
return desiredBrowser;
}
} create another class named PicoDependencyInjector and paste the code below
package util;
import cucumber.runtime.java.picocontainer.PicoFactory;
public class PicoDependencyInjector extends PicoFactory {
public PicoDependencyInjector() {
addClass(SharedDriver.class);
}
} TestNG Test Runner
The final step is to add TestNG test runner to run the cucumber test.
Basically we need a class that will contained test runner information and testng.xml
First for test runner class, let create a class named testNGCucumberRunner and copy code below
import cucumber.api.CucumberOptions;
import cucumber.api.testng.CucumberFeatureWrapper;
import cucumber.api.testng.PickleEventWrapper;
import cucumber.api.testng.TestNGCucumberRunner;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@CucumberOptions(
plugin = {"pretty","json:target/report/cucumber2.json"},
strict = true,
features = {"src/test/resources/features"},
glue = {"steps", "util"}
)
public class UITest {
private TestNGCucumberRunner testNGCucumberRunner;
@BeforeClass(alwaysRun = true)
public void setUpClass() throws Exception {
testNGCucumberRunner = new TestNGCucumberRunner(this.getClass());
}
@Test(groups = "Cucumber", description = "Runs Cucumber Feature", dataProvider = "scenarios")
public void scenario(PickleEventWrapper pickleEvent, CucumberFeatureWrapper cucumberFeature) throws Throwable {
testNGCucumberRunner.runScenario(pickleEvent.getPickleEvent());
}
@DataProvider
public Object[][] scenarios() {
return testNGCucumberRunner.provideScenarios();
}
@AfterClass(alwaysRun = true)
public void tearDownClass() throws Exception {
testNGCucumberRunner.finish();
}
} in the root folder, create a file named testng.xml where we going to put testng configuration to this file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Google Suite" verbose="1" thread-count="1" parallel="tests" configfailurepolicy="continue">
<test name="Google UI automation">
<classes>
<class name="UITest">
<methods>
<include name="scenario"/>
</methods>
</class>
</classes>
</test>
</suite> Run Test
There are few ways to run the test.
We going to run the test using terminal and Maven
- go to the root folder of the project
- run this command
mvn clean test
Conclusion
- Cucumber-jvm support JUnit and TestNG as their test runner
- The only difference between Junit and TestNG are
- Test runner class
- testng.xml