Selenium Java Maven Cross-Browser tests in Docker using Selenoid
In this tutorial the Cross-Browser tests from the tutorial of the Keyword-Driven approach will be executed in Docker containers using Selenoid. This way it is possible to execute the tests on different versions of the same user, without having to install all of them on a client.
Table of contents
Start Selenoid and Selenoid UI using yml-file
Refactor the browser class to match the remote driver
Run the tests on Selenoid
Make the execution of the tests parallel
Download source code
Next step
Start Selenoid and Selenoid UI using yml-file
To run Selenoid it is necessary to have a docker installed, either on the local machine or on the server that will execute the automated Selenium tests. After installing Docker the images for Selenoid and Selenoid-UI have to be downloaded from the docker hub using the following commands:
docker pull aerokube/selenoid && docker pull aerokube/selenoid-ui
On the executing machine a folder for Selenoid has to be created and contain a file called browsers.json. The JSON-file contains the information about the browsers that will be used by Selenoid and the images from Docker hub that will be used to run them. Before starting Selenoid the images have to be downloaded using the docker command as well. In this tutorial the browsers Chrome, Firefox, Opera and Edge will be used in the versions and the images for them can be found in Docker hub at the following links:
Chrome | https://hub.docker.com/r/selenoid/chrome |
Firefox | https://hub.docker.com/r/selenoid/firefox |
Opera | https://hub.docker.com/r/selenoid/opera |
Edge | https://hub.docker.com/r/browsers/edge |
This tutorial will use two versions of every browser which can be pulled using the following commands:
docker pull selenoid/chrome:88.0
docker pull selenoid/chrome:87.0
docker pull selenoid/firefox:85.0
docker pull selenoid/firefox:84.0
docker pull selenoid/opera:73.0
docker pull selenoid/opera:72.0
docker pull browsers/edge:89.0
docker pull browsers/edge:88.0
After downloading the images the browser.json file has to be configured. For the given browsers and versions the file should look like this:
{
"chrome": {
"default": "88.0",
"versions": {
"88.0": {
"image": "selenoid/chrome:88.0",
"port": "4444",
"tmpfs": {"/tmp":"size=512m"}
},
"87.0": {
"image": "selenoid/chrome:87.0",
"port": "4444",
"tmpfs": {"/tmp":"size=512m"}
}
}
},
"firefox": {
"default": "85.0",
"versions": {
"85.0": {
"image": "selenoid/firefox:85.0",
"port": "4444",
"path": "/wd/hub",
"tmpfs": {"/tmp":"size=512m"}
},
"84.0": {
"image": "selenoid/firefox:84.0",
"port": "4444",
"path": "/wd/hub",
"tmpfs": {"/tmp":"size=512m"}
}
}
},
"opera": {
"default": "73.0",
"versions": {
"73.0": {
"image": "selenoid/opera:73.0",
"port": "4444",
"tmpfs": {"/tmp":"size=512m"}
},
"72.0": {
"image": "selenoid/opera:72.0",
"port": "4444",
"tmpfs": {"/tmp":"size=512m"}
}
}
},
"MicrosoftEdge": {
"default": "89.0",
"versions": {
"89.0": {
"image": "browsers/edge:89.0",
"port": "4444",
"tmpfs": {"/tmp":"size=512m"}
},
"88.0": {
"image": "browsers/edge:88.0",
"port": "4444",
"tmpfs": {"/tmp":"size=512m"}
}
}
}
}
After downloading the Docker images and configuring the bowser.json, the containers for Selenoid and Selenoid-UI have to be started with a single command or using a YML-file. Using the YML-file is recommended but for a simple POC it is possible to use the following commands for first starting Selenoid and after that the UI. Here it is necessary to set the Path for /etc/selenoid to the folder containing the browsers.json on the host system.
docker run --rm -d --name selenoid -p 4444:4444 -v C:\PATH\TO\BROWSER\JSON\FOLDER:/etc/selenoid -v /var/run/docker.sock:/var/run/docker.sock aerokube/selenoid
docker run --rm -d --name selenoid-ui --link selenoid -p 8080:8080 aerokube/selenoid-ui --selenoid-uri=http://selenoid:4444
For a stable live system it is recommended to use a yml-file to start the docker containers. Here the path to the folder containing the bowser.json file has to be configured as well. In the end the YML-file should look like this:
version: '3'
services:
selenoid:
image: "aerokube/selenoid"
network_mode: bridge
ports:
- "4444:4444"
volumes:
- C:\PATH\TO\BROWSER\JSON\FOLDER:/etc/selenoid
- /var/run/docker.sock:/var/run/docker.sock
selenoid-ui:
image: "aerokube/selenoid-ui"
network_mode: bridge
ports:
- "8080:8080"
links:
- selenoid
command:
["--selenoid-uri", "http://selenoid:4444"]
With the YML-file set up the docker containers can easily be started using a single docker compose command:
docker-compose -f "docker-compose.yml" up -d --build
After starting the containers the Selenoid-UI can be reached using any browser and calling http://localhost:8080. When opening the capabilities in the top right corner of the page there is a drop-down list containing all browsers and versions. By selecting a browser and clicking on the create session button a docker container with the selected image will be started and can be used for manual tests. Automated tests will use the same containers, so the images should be checked with a manual start once after adding a new browser.
Refactor the browser class to match the remote driver
After starting Selenoid and making sure the containers for the browsers are working the browser.java file in the Selenium framework has to be refactored to implement the remote driver that will start the tests with Selenoid. The framwork should support starting the tests on the local machine and on selenoid using the mvn test command. By adding the browser name only as -Dbrowsers the tests will start locally. For Selenoid execution the version has to be added to the browser so calling the tests would look like mvn test -Dbrowsers=”chrome:88.0,chrome87.0″.
The remote driver to start a test on Selenoid needs some information as desired capabilities. The browser name and the browser version are necessary attributes, additional information like enabling a VNC access or recording a video of the test can be activated with the desired capabilities as well. The remote driver needs to start the tests with the URL http://localhost:4444/wd/hub. The port would have to be changed if you configured another port in the YML-file or the docker start command. In the browser.java file the commands for every browser have to be extended, if the browser name in the variable contains :version, to create the remote driver. In the end, the code of the browser.java file should look like this:
package com.tws.testframework.framework;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.opera.OperaDriver;
import java.util.*;
import java.net.*;
import org.openqa.selenium.remote.*;
import org.openqa.selenium.remote.DesiredCapabilities;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.util.concurrent.TimeUnit;
public class Browser{
public WebDriver driver;
public Browser(String browserName, int implicyWait) throws Exception{
if(browserName.toLowerCase().startsWith("chrome")){
if(browserName.toLowerCase().contains(":")){
String[] browserType = browserName.toLowerCase().split(":");
final DesiredCapabilities browser = DesiredCapabilities.chrome();
browser.setCapability("enableVNC", true);
browser.setCapability("browser", browserType[0]);
browser.setCapability("version", browserType[1]);
driver = new RemoteWebDriver(new URL(
"http://localhost:4444/wd/hub"
), browser);
}else{
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
}
}else if(browserName.toLowerCase().startsWith("firefox") || browserName.toLowerCase().startsWith("ff")){
if(browserName.toLowerCase().contains(":")){
String[] browserType = browserName.toLowerCase().split(":");
FirefoxOptions options = new FirefoxOptions();
options.setCapability("enableVNC", true);
options.setCapability("browser", browserType[0]);
options.setCapability("version", browserType[1]);
driver = new RemoteWebDriver(new URL(
"http://localhost:4444/wd/hub"
), options);
}else{
WebDriverManager.firefoxdriver().setup();
driver = new FirefoxDriver();
}
}else if(browserName.toLowerCase().startsWith("edge") || browserName.toLowerCase().startsWith("eg")){
if(browserName.toLowerCase().contains(":")){
String[] browserType = browserName.toLowerCase().split(":");
EdgeOptions options = new EdgeOptions();
options.setCapability("enableVNC", true);
options.setCapability("browser", browserType[0]);
options.setCapability("version", browserType[1]);
driver = new RemoteWebDriver(new URL(
"http://localhost:4444/wd/hub"
), options);
}else{
WebDriverManager.edgedriver().setup();
driver = new EdgeDriver();
}
}else if(browserName.toLowerCase().startsWith("opera") || browserName.toLowerCase().startsWith("op")){
if(browserName.toLowerCase().contains(":")){
String[] browserType = browserName.toLowerCase().split(":");
final DesiredCapabilities browser = DesiredCapabilities.opera();
browser.setCapability("enableVNC", true);
browser.setCapability("browser", browserType[0]);
browser.setCapability("version", browserType[1]);
driver = new RemoteWebDriver(new URL(
"http://localhost:4444/wd/hub"
), browser);
}else{
WebDriverManager.operadriver().setup();
driver = new OperaDriver();
}
}else{
System.out.println("The browser " + browserName + " is not supported by this framework.");
}
driver.manage().timeouts().implicitlyWait(implicyWait, TimeUnit.SECONDS);
driver.manage().window().maximize();
}
}
Run the tests on Selenoid
With the new browser.java file in place, the framework is ready to run the tests on Selenoid and on the local machine. To start the tests for Chrome, Firefox and Edge on the local device run the tests with the following command:
mvn test -Dbrowsers="chrome,firefox,opera,edge"
To run the tests with Selenoid with both of the configured versions for all four browsers start the tests like this:
mvn test -Dbrowsers="chrome:88.0,chrome:87.0,firefox:85.0,firefox:84.0,opera:73.0,opera:72.0,edge:89.0,edge:88.0"
In the Selenoid UI at http://localhost:8080 a maximum of 5 containers will be started simultaneously if a parallel execution is enabled in the framework. With the current setting the tests will be started one after another and for each test a container will be created to execute the tests. After the execution is done, the containers will automatically be deleted. To speed up the test execution it is now time to add a parallel execution to the framework.
Make the execution of the tests parallel
Due to the fact that the framework already uses TestNG Data Provider to execute the tests with a data driven approach its very easy to bring a parallel execution in place. In theory, it would be enough to add the parameter “parallel = true” to the @DataProvider annotation. This parameter has to be added in the file SearchProvider.java and if other Data Providers are in place to every provider of the framework. The file SearchProvider.java for parallel execution should look like this:
package com.tws.testframework.dataprovider;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.opencsv.*;
import com.opencsv.exceptions.CsvException;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class SearchProvider
{
@DataProvider (name = "search-data-provider", parallel = true)
public Object[][] dpSearch(){
String [] browsers = BrowserProvider.browser();
String [][] fileData = null;
String [][] data;
FileReader filereader = null;
String pathToCSVFile = "src/test/java/com/tws/testframework/testdata/search.csv";
try{
filereader = new FileReader(pathToCSVFile);
}catch(Exception e){
System.out.println("File not found.");
}
CSVParser parser = new CSVParserBuilder().withSeparator(';').build();
try (CSVReader reader = new CSVReaderBuilder(filereader).withCSVParser(parser).build();) {
List<String[]> lines = reader.readAll();
fileData = lines.toArray(new String[lines.size()][]);
}catch(Exception e){
System.out.println("File not found.");
}
data = new String[fileData.length * browsers.length][fileData[0].length + 1];
int countResult = 0;
for(int countBrowser = 0; countBrowser < browsers.length; countBrowser++){
for (String[] csvRowData : fileData) {
for(String csvData : csvRowData){
data[countResult][countEntry] = csvData;
countEntry++;
}
data[countResult][0] = browsers[countBrowser];
countResult++;
}
}
return data;
}
}
If the tests are executed now with the mvn test command it will start several browsers but immediately crash again. That’s because of the implementation of the WebDriver in the test class, that is defined as a global variable and used by every of the parallel executions at the same time. So every test will override the WebDriver of the previously started test. To avoid this and bring the acutal parallel execution in place, it is necessary to refacor the FirstTest.java file and create a HashMap for the WebDrivers. To edentify the execution the information of the DataProvider have to be extended with a counter for every data set that will be started. After the refactoring the SearchProvider.java class containing the data row counter should look like this:
package com.tws.testframework.dataprovider;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.opencsv.*;
import com.opencsv.exceptions.CsvException;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class SearchProvider
{
@DataProvider (name = "search-data-provider", parallel = true)
public Object[][] dpSearch(){
String [] browsers = BrowserProvider.browser();
String [][] fileData = null;
String [][] data;
FileReader filereader = null;
String pathToCSVFile = "src/test/java/com/tws/testframework/testdata/search.csv";
try{
filereader = new FileReader(pathToCSVFile);
}catch(Exception e){
System.out.println("File not found.");
}
CSVParser parser = new CSVParserBuilder().withSeparator(';').build();
try (CSVReader reader = new CSVReaderBuilder(filereader).withCSVParser(parser).build();) {
List<String[]> lines = reader.readAll();
fileData = lines.toArray(new String[lines.size()][]);
}catch(Exception e){
System.out.println("File not found.");
}
data = new String[fileData.length * browsers.length][fileData[0].length + 2];
int countResult = 0;
for(int countBrowser = 0; countBrowser < browsers.length; countBrowser++){
for (String[] csvRowData : fileData) {
int countEntry = 2;
for(String csvData : csvRowData){
data[countResult][countEntry] = csvData;
countEntry++;
}
data[countResult][1] = browsers[countBrowser];
data[countResult][0] = String.valueOf(countResult);
countResult++;
}
}
return data;
}
}
The refactored FirstTest.java class will contain a HashMap for the drivers that has to be implemented in the test and the @After annotation. The source code of the FirstTest.java file should look like this:
package com.tws.testframework.tests;
import com.tws.testframework.framework.Browser;
import com.tws.testframework.dataprovider.SearchProvider;
import com.tws.testframework.keywords.*;
import org.openqa.selenium.WebDriver;
import org.testng.annotations.Test;
import org.testng.annotations.AfterMethod;
import org.testng.ITestResult;
import java.util.HashMap;
public class FirstTest{
HashMap<Integer, WebDriver> driverMap = new HashMap<Integer, WebDriver>();
private WebDriver initializeBrowser(String testNo, String browsername) throws Exception{
com.tws.testframework.framework.Browser browser = new Browser(browsername, 10);
driverMap.put(Integer.parseInt(testNo), browser.driver);
return browser.driver;
}
@Test(dataProvider = "search-data-provider", dataProviderClass = SearchProvider.class)
private void testCase(String testNo, String browsername, String searchTerm, String searchResultString, String firstResultHeader, String firstResultSummary) throws Exception{
WebDriver driver = initializeBrowser(testNo, browsername);
OpenPage.openTwsMain(driver);
Search.searchForTerm(driver, searchTerm);
Search.checkFirstSearchResult(driver, searchResultString, firstResultHeader, firstResultSummary);
Thread.sleep(10000);
}
@AfterMethod
private void closeBrowsers(ITestResult result){
WebDriver driver = driverMap.get(Integer.parseInt(result.getParameters()[0].toString()));
driver.quit();
}
}
With the refactored WebDriver variable and the set counter in the Data Provider in place the parallel execution will work as expected. By starting the tests with the command of the previous step, Selenoid will start 5 docker containers at once to execute the test. Every time one of the tests is done the container will be removed and a new container for the next test will be started. The maximal amount of containers can always be reconfigured in Selenoid to run more tests at once and speed up the execution, if the host is strong enough to handle all of them.
Download source code
You can find the fully commented source code including the Docker YML and a Batch file to start the Selenoid containers on Windows at GitHub:
https://github.com/TestautomationPopp/Selenium-corss-browser-with-selenoid
Next step
The next step is to automatically start the tests and add new docker images using Jenkins.