Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Mastering Selenium WebDriver 3.0
Mastering Selenium WebDriver 3.0

Mastering Selenium WebDriver 3.0: Boost the performance and reliability of your automated checks by mastering Selenium WebDriver , Second Edition

eBook
Mex$561.99 Mex$803.99
Paperback
Mex$1004.99
Subscription
Free Trial

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
OR
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Table of content icon View table of contents Preview book icon Preview Book

Mastering Selenium WebDriver 3.0

Creating a Fast Feedback Loop

One of the main problems you hear people talking about with Selenium is how long it takes to run all of their tests; I have heard figures ranging from a couple of hours to a couple of days. In this chapter, we will have a look at how we can speed things up and get the tests that you are writing to run both quickly and regularly.

Another problem that you may come across is getting other people to run your tests; this is usually because it is a pain to set up the project to work on their machine and it's too much effort for them. As well as making things run quickly, we are going to make it very easy for others to check out your code and get themselves up and running.

How does this create a fast feedback loop?  

Well, first of all, allow me to explain what a fast feedback loop is. As developers change or refactor code, it's possible that they may make a mistake and break something. The feedback loop starts off when they commit code and is complete when they know whether their code changes have worked as expected, or something has been broken. We want to make this feedback loop as fast as possible, so ideally a developer will be running all of the tests that are available before every check in. They will then know whether the changes they made to the code have broken something before the code leaves their machine.

Eventually, we want to get to the point where developers are updating tests that fail because the functionality has changed as they go. The eventual code to turn the tests into living documentation, we will talk about a bit more about in Chapter 2, Producing the Right Feedback When Failing.

In this chapter, we are going to start by creating a basic test framework. What am I going to need? The software and browser versions used to write the code in this chapter are as follows:

  • Java SDK 8
  • Maven 3.5.3
  • Chrome 66
  • Firefox 60

It's a good idea to make sure that you atleast update to these versions to make sure everything works for you.

Making it easy for developers to run tests

Ideally, we want our tests to run every time somebody pushes code to the central code repository; part of doing this is ensuring that it's very easy to run our tests. If somebody can just check out our code base and run one command and have all of the tests just work, it means they are far more likely to run them.

We are going to make this easy by using Apache Maven. To steal a quote from the Maven documentation:

"Maven is an attempt to apply patterns to a project's build infrastructure in order to promote comprehension and productivity by providing a clear path in the use of best practices."

Maven is a tool that can be used to build and manage Java projects (including downloading any dependencies that you require) and is used in many companies as part of the standard enterprise infrastructure. Maven is not the only solution to this problem (for example, Gradle is a very powerful alternative that is on par with Maven in many areas and exceeds it in a few), but it is one that you are most likely to see on the ground and one that most Java developers will have used at some point in their careers.

One of the major plus points is that it encourages developers to use a standardized project structure that makes it easy for people who know Maven to navigate around the source code; it also makes it very easy to plug into a CI system (such as Jenkins or TeamCity), as all the major ones understand Maven POM files.

How does this make it easy for developers to run tests? Well, when we have set our project up using Maven, they should be able to check out our test code and simply type mvn clean verify into a Terminal window. This will automatically download all dependencies, set up the class path, and run all of the tests.

It doesn't really get much easier than that.

Building our test project with Apache Maven

Getting a full working Maven install up and running is not within the scope of this book. It's okay though, don't panic! The Apache Software Foundation has you covered, it has a guide to setting up Maven up in just five minutes! See the following link:

http://maven.apache.org/guides/getting-started/maven-in-five-minutes.html

If you are running the Debian derivative of Linux, it is as easy as using this command:

sudo apt-get install maven 

Or if you are running a Mac with Homebrew, it is just this code:

brew install maven 

Once you have Maven installed and working, we will start our Selenium project with a basic POM file. We are going to start by creating a basic Maven directory structure and then creating a file called pom.xml in it. Take a look at the following screenshot:

There are two main testing frameworks that you will come across in a Java environment; JUnit and TestNG. I personally find TestNG to be easier to get up and running out of the box, but I find JUnit to be more extensible. TestNG certainly seems to be popular on the Selenium mailing list, with many threads asking questions about it; you don't often see JUnit questions any more.

I'm not going to suggest either one as the right choice, as they are both capable frameworks that you will probably come across in the enterprise world. Since TestNG seems to be the more popular option, we will focus on a TestNG implementation in this chapter.

If you prefer JUnit, you will want to have a look at Appendix 2Working with jUnit. In this, we will implement the same base project, but we will use JUnit instead of TestNG. This means instead of worrying about which one is best, you can have a look at a TestNG implementation and a JUnit implementation. You can then choose which one you prefer and read the relevant section.

So, to start with, let's have a look at a basic POM code for a TestNG-based Maven project:

<?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"
>

<groupId>com.masteringselenium.demo</groupId>
<artifactId>mastering-selenium-testng</artifactId>
<version>DEV-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>

<name>Mastering Selenium TestNG</name>
<description>A basic Selenium POM file</description>
<url>http://www.masteringselenium.com</url>

<properties>
<project.build.sourceEncoding>UTF-
8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-
8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!-- Dependency versions -->
<selenium.version>3.12.0</selenium.version>
<testng.version>6.14.3</testng.version>
<!-- Plugin versions -->
<maven-compiler-plugin.version>3.7.0</maven-compiler-
plugin.version>
<maven-failsafe-plugin.version>2.21.0</maven-failsafe-
plugin.version>
<!-- Configurable variables -->
<threads>1</threads>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
<version>${maven-compiler-plugin.version}</version>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-remote-driver</artifactId>
<version>${selenium.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>

What you are seeing here is mainly Maven boilerplate code. The groupId, artifactId, and version are subject to standard naming conventions, which are as follows :

  • groupId should be a domain that you own/control and is entered in reverse
  • artifactId is the name that will be allocated to your JAR file, so remember to make it what you want your JAR file to be called
  • version should always be a number with -SNAPSHOT appended to the end; this shows that it is currently a work in process

We have added the Maven compiler plugin so that we can define the version of Java that is required to compile the code we are going to write. We have opted to require Java 8, since this is the minimum version of Java currently supported by Selenium.

Next, we have the libraries that our code depends upon; these are stored in the dependencies block. To get us started, we have added a dependency for Selenium and a dependency for TestNG. Note that we have given them a scope of test; this ensures that these dependencies are only loaded into the classpath when tests are run and are never packaged up in any artifacts that are generated as part of the build process.

We have used Maven properties to set our dependency versions. This is not required, but it's a common Maven convention. The idea is that it's easier to update the versions of things in your POM if they are all declared in one place. XML can be very verbose and wading through the POM finding each dependency or plugin version that we want to update can be time consuming, especially when you start using Maven profiles.

You can now open up this POM file using your IDE (in this book, I'm assuming that you are using IntelliJ IDEA, but any modern IDE should be able to open up a POM file and create a project from it).

We now have the basis of our Selenium project. The next step is to create a basic test that we can run using Maven. Start by creating a src/test/java directory. Your IDE should automatically work out that this directory is a test sources directory. We then need to create a new package in this directory called com.masteringselenium. Finally, inside this package, we will create a file called BasicTest.java. Into this file we are going to put the following code:

package com.masteringselenium;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.Test;

public class BasicTest {

private ExpectedCondition<Boolean> pageTitleStartsWith(final
String searchString) {
return driver -> driver.getTitle().toLowerCase().
startsWith(searchString.toLowerCase());
}

private void googleExampleThatSearchesFor(final
String searchString) {

WebDriver driver = new FirefoxDriver();

driver.get("http://www.google.com");

WebElement searchField = driver.findElement(By.name("q"));

searchField.clear();
searchField.sendKeys(searchString);

System.out.println("Page title is: " + driver.getTitle());

searchField.submit();

WebDriverWait wait = new WebDriverWait(driver, 10, 100);
wait.until(pageTitleStartsWith(searchString));

System.out.println("Page title is: " + driver.getTitle());

driver.quit();
}

@Test
public void googleCheeseExample() {
googleExampleThatSearchesFor("Cheese!");
}

@Test
public void googleMilkExample() {
googleExampleThatSearchesFor("Milk!");
}
}

These two tests should be quite familiar; it's the basic Google cheese scenario with all the main grunt work abstracted out into a method that we are able to call multiple times with different search terms. We now have everything we need to run our tests. To kick them off, type the following command into a Terminal window:

mvn clean verify 

You will now see Maven downloading all of the Java dependencies from Maven central. When it is completed, it will build the project and then run the tests.

If you have problems downloading the dependencies, try adding a -U to the end of the command; this will force Maven to check the Maven central repositories for updated libraries.

You will now see Firefox load up and then your test will fail, since with Selenium 3 all of the driver binaries (the part that actually drives the browser) are no longer bundled with Selenium. You will now have to download the relevant binaries to be able to run your tests.

For now, we will download a binary and then pass an environmental variable into the JVM so that we can get this initial test running. Later on, we will take a look at a slightly more streamlined way to do this that will automatically download the required driver binaries.

We are running our tests against Firefox, so we will need to download the geckodriver binary; the latest one is available at https://github.com/mozilla/geckodriver/releases.

Now that we have a usable driver binary, we need to tell Selenium where to find it. Luckily, the Selenium team have already provided us with a way to do this. When Selenium starts up and tries to instantiate a driver object, it will look for a system property that holds the location of the required executable. These system properties are in the format WebDriver.<DRIVER_TYPE>.driver. To get our test working, all we need to do is pass this system property on the command line:

mvn clean verify -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

This time, Firefox should load up correctly, run your tests without any error, and finally give you a pass.

If you are still having problems, check the version of Firefox that you are using. The code in this chapter has been written against Firefox 60. If you are using earlier versions, you may suffer from patchy geckodriver support, and you may see some errors.

We now have a very basic project set up to run a couple of very basic tests using Maven. Right now, this will run very quickly, but as you start adding more and more tests to your project, things are going to start to slow down. To try and mitigate this problem, we are going to utilize the full power of your machine by running your tests in parallel.

Running your tests in parallel

Running your tests in parallel means different things to different people, as it can mean either of the following:

  • Run all of your tests against multiple browsers at the same time
  • Run your tests against multiple instances of the same browser

Should we run our tests in parallel to increase coverage?

I'm sure that when you are writing automated tests, to make sure things work with the website you are testing, you are initially told that your website has to work on all browsers. The reality is that this is just not true. There are many browsers out there and it's just not feasible to support everything. For example, will your AJAX-intensive site that has the odd flash object work in the Lynx browser?

Lynx is a text-based web browser that can be used in a Linux Terminal window and was still in active development in 2014.

The next thing you will hear is, "OK, well, we will support every browser supported by Selenium." Again, that's great, but we have problems. Something that most people don't realize is that the core Selenium teams official browser support is the current browser version, and the previous version at the time of release of a version of Selenium. In practice, it may well work on older browsers and the core team does a lot of work to try and make sure they don't break support for older browsers. However, if you want to run a series of tests on Internet Explorer 6, Internet Explorer 7, or even Internet Explorer 8, you are actually running tests against browsers that are not officially supported by Selenium.

We then come to our next set of problems. Internet Explorer is only supported on Windows machines, and you can have only one version of Internet Explorer installed on a Windows machine at a time.

There are hacks to install multiple versions of Internet Explorer on the same machine, but you will not get accurate tests if you do this. It's much better to have multiple operating systems running with just one version of Internet Explorer.

Safari is only supported on OS X machines, and, again, you can have only one version installed at a time.

There is an old version of Safari for Windows hidden away in Apple's archives, but it is no longer actively supported and shouldn't be used.

It soon becomes apparent that even if we do want to run all of our tests against every browser supported by Selenium, we are not going to be able to do it on one machine.

At this point, people tend to modify their test framework so that it can accept a list of browsers to run against. They write some code that detects, or specifies, which browsers are available on a machine. Once they have done this, they start running all of their tests over a few machines in parallel and end up with a matrix that looks like this:

This is great, but it doesn't get around the problem that there is always going to be one or two browsers you can't run against your local machine, so you will never get full cross-browser coverage. Using multiple different driver instances (potentially in multiple threads) to run against different browsers has given us slightly increased coverage. We still don't have full coverage though.

We also suffer some side effects by doing this. Different browsers run tests at different speeds because JavaScript engines in all browsers are not equal. We have probably drastically slowed down the process of checking that the code works before you push it to a source code repository.

Finally, by doing this we can make it much harder to diagnose issues. When a test fails, you now have to work out which browser it was running against, as well as why it failed. This may only take a minute of your time, but all those minutes do add up.

So, why don't we just run our tests against one type of browser for the moment. Let's make that test run against that browser nice and quickly, and then worry about cross-browser compatibility later.

It's probably a good idea to just pick one browser to run our tests against on our development machines. We can then use a CI server to pick up the slack and worry about browser coverage as part of our build pipeline. It's probably also a good idea to pick a browser with a fast JavaScript engine for our local machines.

Parallel tests with TestNG

The TestNG examples used in this chapter will be using TestNG Version 6.14.3 and the Maven Failsafe Plugin Version 2.21.0. If you use older versions of these components, the functionality that we are going to use may not be available.

To start, we are going to make some changes to our POM file. We are going to add a threads property, which will be used to determine the number of parallel threads used to run our checks. Then, we are going to use the Maven Failsafe Plugin to configure TestNG:

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>
UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!-- Dependency versions -->
<selenium.version>3.12.0</selenium.version>
<testng.version>6.14.3</testng.version>
<!-- Plugin versions -->
<maven-compiler-plugin.version>3.7.0
</maven-compiler-plugin.version>
<maven-failsafe-plugin.version>2.21.0
</maven-failsafe-plugin.version>
<!-- Configurable variables -->
<threads>1</threads>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<parallel>methods</parallel>
<threadCount>${threads}</threadCount>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
When using the Maven Failsafe Plugin, the integration-test goal will ensure that your tests run in the integration test phase. The verify goal ensures that the Failsafe Plugin checks the results of the checks run in the integration-test phase, and fails the build if something did not pass. If you don't have the verify goal, the build will not fail!

TestNG supports parallel threads out of the box; we just need to tell it how to use them. This is where the Maven Failsafe Plugin comes in. We are going to use it to configure our parallel execution environment for our tests. This configuration will be applied to TestNG if you have TestNG as a dependency; you don't need to do anything special.

In our case, we are interested in parallel and the threadCount configuration settings. We have set parallel to methods. This will search through our project for methods that have the @Test annotation and will collect them all into a great big pool of tests. The Failsafe Plugin will then take tests out of this pool and run them. The number of tests that will be run concurrently will depend on how many threads are available. We will use the threadCount property to control this.

It is important to note that there is no guarantee in which order tests will be run.

We are using the threadCount configuration setting to control how many tests we run in parallel, but as you may have noticed we have not specified a number. Instead, we have used the Maven variable ${threads}, this will take the value of the maven property threads that we defined in our properties block and pass it into threadCount.

Since threads is a Maven property, we are able to override its value on the command line by using the -D switch. If we do not override its value, it will use the value we have set in the POM as a default.

So, if we run the following command, it will use the default value of 1 in the POM file.:

mvn clean verify -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

However, if we use this next command, it will overwrite the value of 1 stored in the POM file and use the value 2 instead:

mvn clean verify -Dthreads=2 -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

As you can see, this gives us the ability to tweak the number of threads that we use to run our tests without making any code changes at all.

We have used the power of Maven and the Maven Failsafe Plugin to set the number of threads that we want to use when running our tests in parallel, but we still have more work to do!

If you run your tests right now, you will see that even though we are supplying multiple threads to our code, all the tests still run in a single thread. Selenium is not thread safe, so we need to write some code that will make sure that each Selenium instance runs in its own isolated thread and does not leak over to other threads.

Previously, we were instantiating an instance of FirefoxDriver in each of our tests. Let's pull this out of the test, and put browser instantiation into its own class called DriverFactory. We will then add a class called DriverBase that will deal with the marshaling of the threads.

We are going to now build a project structure that looks like this:

First of all, we need to create our DriverFactory class by using the following code:

package com.masteringselenium;

import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.remote.RemoteWebDriver;

public class DriverFactory {

private RemoteWebDriver webDriver;

private final String operatingSystem =
System.getProperty("os.name").toUpperCase();
private final String systemArchitecture =
System.getProperty("os.arch");

RemoteWebDriver getDriver() {
if (null == webDriver) {
System.out.println(" ");
System.out.println("Current Operating System: " +
operatingSystem);
System.out.println("Current Architecture: " +
systemArchitecture);
System.out.println("Current Browser Selection:
Firefox"
);
System.out.println(" ");
webDriver = new FirefoxDriver();
}

return webDriver;
}

void quitDriver() {
if (null != webDriver) {
webDriver.quit();
webDriver = null;
}
}
}

This class holds a reference to a WebDriver object, and ensures that every time you call getDriver() you get a valid instance of WebDriver back. If one has been started up, you will get the existing one. If one hasn't been started up, it will start one for you.

It also provides a quitDriver() method that will perform quit() on your WebDriver object. It also nullifies the WebDriver object held in the class. This prevents errors that would be caused by attempting to interact with a WebDriver object that has been closed.

Note that we are using driver.quit() and not driver.close(). As a general rule of thumb, you should not use driver.close() to clean up. It will throw an error if something happened during your test that caused the WebDriver instance to close early. The close-and-clean-up command in the WebDriver API is driver.quit(). You would normally use driver.close() if your test opens multiple windows and you want to shut some of them.

Next, we need to create a class called DriverBase by using this command:

package com.masteringselenium;

import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class DriverBase {

private static List<DriverFactory> webDriverThreadPool =
Collections.synchronizedList(new ArrayList<DriverFactory>());
private static ThreadLocal<DriverFactory> driverThread;

@BeforeSuite(alwaysRun = true)
public static void instantiateDriverObject() {
driverThread = new ThreadLocal<DriverFactory>() {
@Override
protected DriverFactory initialValue() {
DriverFactory webDriverThread = new DriverFactory();
webDriverThreadPool.add(webDriverThread);
return webDriverThread;
}
};
}

public static RemoteWebDriver getDriver() {
return driverThread.get().getDriver();
}

@AfterMethod(alwaysRun = true)
public static void clearCookies() {
getDriver().manage().deleteAllCookies();
}

@AfterSuite(alwaysRun = true)
public static void closeDriverObjects() {
for (DriverFactory webDriverThread : webDriverThreadPool) {
webDriverThread.quitDriver();
}
}
}

This is a small class that will hold a pool of driver objects. We are using a ThreadLocal object to instantiate our WebDriverThread objects in separate threads. We have also created a getDriver() method that uses the getDriver() method on the DriverFactory object to pass each test a WebDriver instance that it can use.

We are doing this to isolate each instance of WebDriver to make sure that there is no cross contamination between tests. When our tests start running in parallel, we don't want different tests to start firing commands to the same browser window. Each instance of WebDriver is now safely locked away in its own thread.

Since we are using this factory class to start up all our browser instances, we need to make sure that we close them down as well. To do this, we have created a method with an @AfterMethod annotation that will destroy the driver after our test has run. This also has the added advantage of cleaning up if our test fails to reach the line where it would normally call driver.quit(), for example, if there was an error in the test that caused it to fail and finish early.

Note that our @AfterMethod and @BeforeSuite annotations have a parameter of alwaysRun = true set on them. This makes sure that these functions are always run. For example, with our @AfterMethod annotation this makes sure that, even if a test fails, we will call the driver.quit() method. This ensures that we shut down our driver instance which will in turn close the browser. This should reduce the chance of you having some open browser windows left over after your test run if some of your tests fail.

All that is left now is to clean up the code in our basicTest class and change its name to BasicIT. Why have we changed the name of the test? Well, we are going to use the maven-failsafe-plugin to run our tests in the integration-test phase. This plugin picks up files that end in IT by default. If we left the class with a name ending in TEST, it would be picked up by the maven-surefire-plugin. We don't want the maven-surefire-plugin to pick up our tests, that should really be used for unit tests, we want to use the maven-failsafe-plugin instead, so we will use this code:

package com.masteringselenium;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.Test;

public class BasicIT extends DriverBase {

private ExpectedCondition<Boolean> pageTitleStartsWith(final
String searchString) {
return driver -> driver.getTitle().toLowerCase()
.startsWith(searchString.toLowerCase());
}

private void googleExampleThatSearchesFor(final String
searchString) {

WebDriver driver = DriverBase.getDriver();

driver.get("http://www.google.com");

WebElement searchField = driver.findElement(By.name("q"));

searchField.clear();
searchField.sendKeys(searchString);

System.out.println("Page title is: " + driver.getTitle());

searchField.submit();

WebDriverWait wait = new WebDriverWait(driver, 10, 100);
wait.until(pageTitleStartsWith(searchString));

System.out.println("Page title is: " + driver.getTitle());
}

@Test
public void googleCheeseExample() {
googleExampleThatSearchesFor("Cheese!");
}

@Test
public void googleMilkExample() {
googleExampleThatSearchesFor("Milk!");
}
}

We have modified our basic test so that it extends DriverBase. Instead of instantiating a new FirefoxDriver in the test, we are calling DriverBase.getDriver() to get a valid WebDriver instance. Finally, we have removed the driver.quit() from our generic method as this is all done by our DriverBase class now.

If we spin up our test again using this code, you won't notice any difference.:

mvn clean verify -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

However, if you now specify some threads by running this code, you will see that, this time, two Firefox browsers open, both tests run in parallel, and then both browsers are closed again.:

mvn clean verify -Dthreads=2 -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>
If you want to be completely sure that each test is running in a separate thread, you can add the following to your getDriver() method in the DriverFactory class: System.out.println("Current thread: " + Thread.currentThread().getId());.  

This will show the current thread ID so that you can see that the FirefoxDriver instances are running in different threads.
Only seeing one browser start up? In maven-failsafe-plugin, configuration defaults to searching for all files that end with IT.java. If you use filenames that start or end with Test, they will be picked up by the maven-surefire plugin, and the threading configuration will be ignored. Double-check to make sure that your failsafe configuration is correct.

As you may have noticed, with two very small tests such as the ones we are using in our example, you will not see a massive decrease in the time taken to run the complete suite. This is because most of the time is spent compiling the code and loading up browsers, but as you add more tests the decrease in time taken to run the tests becomes more and more apparent.

This is probably a good time to tweak your BasicIT.java and start adding some more tests that look for different search terms, play about with the number of threads, and see how many concurrent browsers you can get up and running at the same time. Make sure that you note down execution times to see what speed gains you are actually getting (they will also be useful later on in this chapter). There will come a point where you reach the limits of your computer's hardware, and adding more threads will actually slow things down rather than making them faster. Tuning your tests to your hardware environment is an important part of running your tests in multiple threads.

So, how can we speed things up even more? Well, starting up a web browser is a computationally intensive task, so we could choose to not close the browser after every test. This obviously has some side effects. You may not be at the usual entry page to your application, and you may have some session information that is not wanted.

If there is a risk of side effects, why are we contemplating it? The reason for doing this is, quite simply, speed. Let's imagine we have a suite of fifty tests. If you are spending 10 seconds loading up and shutting down a browser for each test that you run, reusing browsers will dramatically reduce the amount of time it takes. If we can only spend 10 seconds starting up and shutting down a browser for all fifty tests, we have shaved eight minutes and 10 seconds off our total test time.  

Let's try it and see how it works for us. First, we will try and deal with our session problem. WebDriver has a command that will allow you to clear out your cookies, so we will trigger this after every test. We will then add a new @AfterSuite annotation to close the browser once all of the tests have finished. Take a look at the following code:

package com.masteringselenium;

import com.masteringselenium.config.DriverFactory;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class DriverBase {

private static List<DriverFactory> webDriverThreadPool =
Collections.synchronizedList(new ArrayList<DriverFactory>());
private static ThreadLocal<DriverFactory> driverThread;

@BeforeSuite(alwaysRun = true)
public static void instantiateDriverObject() {
driverThread = new ThreadLocal<DriverFactory>() {
@Override
protected DriverFactory initialValue() {
DriverFactory webDriverThread = new DriverFactory();
webDriverThreadPool.add(webDriverThread);
return webDriverThread;
}
};
}

public static RemoteWebDriver getDriver() {
return driverThread.get().getDriver();
}

@AfterMethod(alwaysRun = true)
public static void clearCookies() {
try {
getDriver().manage().deleteAllCookies();
} catch (Exception ex) {
System.err.println("Unable to delete cookies: " + ex);
}
}

@AfterSuite(alwaysRun = true)
public static void closeDriverObjects() {
for (DriverFactory webDriverThread : webDriverThreadPool) {
webDriverThread.quitDriver();
}
}
}

The first addition to our code is a synchronized list where we can store all our instances of WebDriverThread. We have then modified our initialValue() method to add each instance of WebDriverThread that we create to this new synchronized list. We have done this to enable us to keep track of our threads.  

Next, we have renamed our @AfterSuite method to ensure that the method names stay as descriptive as possible. It is now called closeDriverObjects(). This method does not just close down the instance of WebDriver that we are using as it did previously. Instead, it iterates through our webDriverThreadPool list, closing every threaded instance that we are keeping track of.

We don't actually know how many threads we are going to have run since this will be controlled by Maven. This is not an issue though, as this code has been written to make sure that we don't have to know. What we do know is that when our tests are finished, each WebDriver instance will be closed down cleanly and without errors, all thanks to the use of the webDriverThreadPool list.

Finally, we have added @AfterMethod called clearCookies() that will clear down the browser's cookies after each test. This should reset the browser to a neutral state without closing it so that we can start another test safely. 

Have a go at tweaking your BasicIT.java again by adding some more tests that look for different search terms. Based on your previous experimentation, you will probably have a rough idea of what the sweet spot for your hardware is. Time how long it takes to execute your tests again when you only close all the browsers down when all the tests have finished executing; how much time did you shave off your execution time?

There are no silver bullets

As with everything, keeping your browser windows open while you run all of your tests will not work in every instance.

Sometimes, you may have a site that sets server-side cookies that Selenium is unaware of. In this case, clearing out your cookies may have no effect and you may find that closing down the browser is the only way to ensure a clean environment for each test.

If you use InternetExplorerDriver, you will probably find that when you use slightly older versions of Internet Explorer (for example, Internet Explorer 8 and Internet Explorer 9), your tests will get slower and slower until they grind to a halt. Unfortunately, older versions of IE are not perfect and they do have some memory leak issues.

Using InternetExplorerDriver does exacerbate these issues because it is really stressing the browser. As a result, it does get a lot of unfair press. It's an excellent bit of code that deals with an awful lot of crap that gets thrown at it.

This is not to say that you can't use this method; you may not see any issues with the application that you are testing. You can of course use a mix of strategies; you can have multiple phases of testing. You can put tests that are able to reuse the browser in the first phase. You can then put tests that need a browser restart in your second phase.

Removing the browser shutdown and startup time after each test really does make a massive difference to the speed of your test runs. From personal experience, I would suggest that you should always try to keep the browser open whenever realistically possible to keep your test times down.

At the end of the day, the only way to be sure if it will work for you is experimentation and hard data. Just remember to do that investigation first. Once you are done, you should then tailor your thread usage to each browser/machine combination or, you should set a baseline that works with everything in your environment.

Multiple browser support

So far, we have parallelized our tests so that we can run multiple browser instances at the same time. However, we are still using only one type of driver, the good old FirefoxDriver. I mentioned problems with Internet Explorer in the previous section, but right now we have no obvious way to run our tests using Internet Explorer. Let's have a look at how we can fix this.

To start with, we will need to create a new Maven property called browser and a new configuration setting inside our Failsafe Plugin configuration called systemPropertyVariables. This is pretty much what is says on the tin; everything defined inside systemPropertyValues will become a system property that is available to your Selenium tests. We are going to use a Maven variable to reference a Maven property so that we can dynamically change this value on the command line.

The following code contains the changes you need to make to your POM:

<properties>
<project.build.sourceEncoding>UTF-
8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-
8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!-- Dependency versions -->
<selenium.version>3.12.0</selenium.version>
<testng.version>6.14.3</testng.version>
<!-- Plugin versions -->
<maven-compiler-plugin.version>3.7.0
</maven-compiler-plugin.version>
<maven-failsafe-plugin.version>2.21.0
</maven-failsafe-plugin.version>
<!-- Configurable variables -->
<threads>1</threads>
<browser>firefox</browser>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<parallel>methods</parallel>
<threadCount>${threads}</threadCount>
<systemPropertyVariables>
<browser>${browser}</browser>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

We now need to create a package where we are going to store our driver configuration code. Into this package, we are going to add a new interface and a new enum. We are also going to move our DriverFactory class into this package to keep things nice and clean. Take a look at the following screenshot:

DriverSetup is a very simple interface that the DriverType class will implement, as shown in the following code:

package com.masteringselenium.config;

import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

public interface DriverSetup {
RemoteWebDriver getWebDriverObject(DesiredCapabilities capabilities);
}

DriverType is where all the work is done, as shown in the following code:

package com.masteringselenium.config;

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
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.ie.InternetExplorerDriver;
import org.openqa.selenium.ie.InternetExplorerOptions;
import org.openqa.selenium.opera.OperaDriver;
import org.openqa.selenium.opera.OperaOptions;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.safari.SafariOptions;

import java.util.HashMap;

public enum DriverType implements DriverSetup {


FIREFOX {
public RemoteWebDriver
getWebDriverObject(DesiredCapabilities
capabilities) {
FirefoxOptions options = new FirefoxOptions();
options.merge(capabilities);

return new FirefoxDriver(options);
}
},
CHROME {
public RemoteWebDriver
getWebDriverObject(DesiredCapabilities
capabilities) {
HashMap<String, Object> chromePreferences = new
HashMap<>
();

chromePreferences.put("profile.password_manager_enabled"
,false);

ChromeOptions options = new ChromeOptions();
options.merge(capabilities);
options.addArguments("--no-default-browser-check");
options.setExperimentalOption("prefs",
chromePreferences);

return new ChromeDriver(options);
}
},
IE {
public RemoteWebDriver
getWebDriverObject(DesiredCapabilities
capabilities) {
InternetExplorerOptions options = new
InternetExplorerOptions();
options.merge(capabilities);
options.setCapability(CapabilityType.ForSeleniumServer.
ENSURING_CLEAN_SESSION, true);
options.setCapability(InternetExplorerDriver.
ENABLE_PERSISTENT_HOVERING, true);
options.setCapability(InternetExplorerDriver.
REQUIRE_WINDOW_FOCUS, true);

return new InternetExplorerDriver(options);
}
},
EDGE {
public RemoteWebDriver
getWebDriverObject(DesiredCapabilities
capabilities) {
EdgeOptions options = new EdgeOptions();
options.merge(capabilities);

return new EdgeDriver(options);
}
},
SAFARI {
public RemoteWebDriver
getWebDriverObject(DesiredCapabilities
capabilities) {
SafariOptions options = new SafariOptions();
options.merge(capabilities);

return new SafariDriver(options);
}
},
OPERA {
public RemoteWebDriver
getWebDriverObject(DesiredCapabilities
capabilities) {
OperaOptions options = new OperaOptions();
options.merge(capabilities);

return new OperaDriver(options);
}
}
}

As you can see, our basic enum allows us to choose one of the default browsers supported by Selenium. Each enum entry implements the getWebDriverObject() method. This allows us to pass in a DesiredCapabilities object that we then merge into an Options object of the relevant driver type. This is then used to instantiate the WebDriver object and return it.

Instantiating a  <DriverType>Driver object with a DeisredCapabilities object is now deprecated.  The new way of doing things is to use a <DriverType>Options object. DesiredCapabilities is still used in various places right now (for example, if you are instantiating a RemoteWebDriver object to connect to a Selenium-Grid, it's still supported), so it hasn't been fully removed.

Let's have a look at the default options that we have set for each driver to help things run smoothly:

  • Chrome: We have a couple of options here to try and keep things running smoothly. Chrome has various command-line switches that can be used when starting Chrome up with ChromeDriver. When we load up Chrome to run our tests, we don't want it asking us whether it can be made the default browser every time it starts, so we have disabled that check. We have also turned off the password manager so that it does not ask if you would like to save your login details every time you have a test that performs a login action.
  • Internet Explorer: InternetExplorerDriver has a lot of challenges; it attempts to work with many different versions of Internet Explorer and generally does a very good job. These options are used to try to ensure that sessions are properly cleaned out when reloading the browser (IE8 is particularly bad at clearing its cache), and then trying to fix some issues with hovering. If you have ever tested an application that needs you to hover over an element to trigger some sort of popup, you have probably seen the popup flickering lots, and had intermittent failures when trying to interact with it. Setting ENABLE_PERSISTENT_HOVERING and requireWindowFocus should work around these issues.
  • Others: The other drivers are relatively new (by comparison), and I haven't really come across any problems with the default set of options, so these are just placeholders that return a default options object.

You don't need to use any of the preceding desired capabilities, but I have found them to be useful in the past. If you don't want to use them, just remove the bits you aren't interested in and set each getWebDriverObject() method up like the FirefoxDriver one. Remember, this is just a starting point for your test framework. You can add in any specific options that you find useful in your tests. This is going to be the place that instantiates a driver object so it's the best place to do it.

Now that everything is in place, we need to rewrite our DriverFactory method. Take a look at the following code:

package com.masteringselenium.config;

import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import static com.masteringselenium.config.DriverType.FIREFOX;
import static com.masteringselenium.config.DriverType.valueOf;

public class DriverFactory {

private RemoteWebDriver webDriver;
private DriverType selectedDriverType;

private final String operatingSystem =
System.getProperty("os.name").toUpperCase();
private final String systemArchitecture =
System.getProperty("os.arch");

public DriverFactory() {
DriverType driverType = FIREFOX;
String browser = System.getProperty("browser",
driverType.toString()).toUpperCase();
try {
driverType = valueOf(browser);
} catch (IllegalArgumentException ignored) {
System.err.println("Unknown driver specified,
defaulting to '"
+ driverType + "'...");
} catch (NullPointerException ignored) {
System.err.println("No driver specified,
defaulting to '"
+ driverType + "'...");
}
selectedDriverType = driverType;
}

public RemoteWebDriver getDriver() {
if (null == webDriver) {
instantiateWebDriver(selectedDriverType);
}

return webDriver;
}

public void quitDriver() {
if (null != webDriver) {
webDriver.quit();
webDriver = null;
}
}

private void instantiateWebDriver(DriverType driverType) {
System.out.println(" ");
System.out.println("Local Operating System: " +
operatingSystem);
System.out.println("Local Architecture: " +
systemArchitecture);
System.out.println("Selected Browser: " +
selectedDriverType);
System.out.println(" ");
DesiredCapabilities desiredCapabilities = new
DesiredCapabilities();
webDriver =
driverType.getWebDriverObject(desiredCapabilities);
}
}

There is quite a lot going on here. First, we have added a new variable called selectedDriverType. We are going to use this to store the type of driver that we want to use to run tests. We have then added a constructor that will determine what selectedDriverType should be when we instantiate the class.  The constructor looks for a system property called browser to work out what sort of DriverType is desired. There is some error handling that will make sure that if we can't identify the requested driver type we always fall back to a default, in this case FirefoxDriver. You can remove this error handling if you would prefer to error every time an invalid driver string is passed in.

We have then added a new method called instantiateWebDriver(), which is very similar to the code that was previously inside getDriver(). The only real difference is that we can now pass a DriverType object to specify which sort of WebDriver object we want. We also now create a DesiredCapabilities object inside this new method because that needs to be passed into the getWebDriverObject() method.

Finally, the getDriver() method has been tweaked to call the new instantiateDriver() method. One other thing that is important to note is that we are no longer passing around a WebDriver object; we are instead passing around a RemoteWebDriver object.  This is because all the drivers now extend RemoteWebDriver by default.  

Let's try it out. First of all, let's check that everything still works like it used to by using the following code:

mvn clean verify -Dthreads=2 -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

This time, you should have seen no difference to the last time you ran it. Let's check the error handling next:

mvn clean verify -Dthreads=2 -Dbrowser=iJustMadeThisUp -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

Again, it should have looked exactly the same as the previous run. We couldn't find an enum entry called IJUSTMADETHISUP, so we defaulted to the FirefoxDriver.

Finally, let's try a new browser:

mvn clean verify -Dthreads=2 -Dbrowser=chrome

You have probably had mixed success with this one; you will see that it tried to start up ChromeDriver, but if you don't have the Chrome Driver executable installed on your system that is in your default $PATH, it most likely threw an error saying that it couldn't find the Chrome Driver executable.

You can fix this by downloading the Chrome Driver binary and then providing the path to the binary using -Dwebdriver.chrome.driver=<PATH_TO_CHROMEDRIVER_BINARY>, as we did previously with geckodriver. This isn't really making our tests easy to run out of the box for developers, though. It looks as if we have more work to do.

Downloading WebDriver binaries automatically

I came across this problem a few years ago, and at the time there wasn't an easy way to get hold of the binaries using Maven. I didn't find an elegant solution to this problem, so I did what anybody who is into open source software would do: I wrote a plugin to do it for me.

This plugin allows you to specify a series of driver binaries to automatically download and remove the manual setup steps. It also means that you can enforce the version of driver binaries that are used, which removes lots of intermittent issues caused by people using different versions of the binaries that can behave differently on different machines.

We are now going to enhance our project to use this plugin; the new project structure will look like this:

Let's start by tweaking our POM; we will use the following code to create a new property that we will call overwrite.binaries and a new property to set the version of the plugin:

<properties>
<project.build.sourceEncoding>UTF-
8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-
8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!-- Dependency versions -->
<selenium.version>3.12.0</selenium.version>
<testng.version>6.14.3</testng.version>
<!-- Plugin versions -->
<driver-binary-downloader-maven-plugin.version>1.0.17
</driver-binary-downloader-maven-plugin.version>
<maven-compiler-plugin.version>3.7.0
</maven-compiler-plugin.version>
<maven-failsafe-plugin.version>2.21.0
</maven-failsafe-plugin.version>
<!-- Configurable variables -->
<threads>1</threads>
<browser>firefox</browser>
<overwrite.binaries>false</overwrite.binaries>
</properties>

We then need to add the driver-binary-downloader plugin by using the following code:

<plugin>
<groupId>com.lazerycode.selenium</groupId>
<artifactId>driver-binary-downloader-maven-plugin</artifactId>
<version>${driver-binary-downloader-maven-plugin.version}
</version>
<configuration>
<rootStandaloneServerDirectory>${project.basedir}
/src/test/resources/selenium_standalone_binaries
</rootStandaloneServerDirectory>
<downloadedZipFileDirectory>${project.basedir}
/src/test/resources/selenium_standalone_zips
</downloadedZipFileDirectory>
<customRepositoryMap>${project.basedir}
/src/test/resources/RepositoryMap.xml
</customRepositoryMap>
<overwriteFilesThatExist>${overwrite.binaries}
</overwriteFilesThatExist>
</configuration>
<executions>
<execution>
<goals>
<goal>selenium</goal>
</goals>
</execution>
</executions>
</plugin>

Finally, we need to add some new system properties to our maven-failsafe-plugin configuration by using the following code:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<parallel>methods</parallel>
<threadCount>${threads}</threadCount>
<systemPropertyVariables>
<browser>${browser}</browser>
<!--Set properties passed in by the driver binary
downloader-->
<webdriver.chrome.driver>${webdriver.chrome.driver}
</webdriver.chrome.driver>
<webdriver.ie.driver>${webdriver.ie.driver}
</webdriver.ie.driver>
<webdriver.opera.driver>${webdriver.opera.driver}
</webdriver.opera.driver>
<webdriver.gecko.driver>${webdriver.gecko.driver}
</webdriver.gecko.driver>
<webdriver.edge.driver>${webdriver.edge.driver}
</webdriver.edge.driver>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>

The plugin runs in the TEST_COMPILE phase by default. The order it is placed in the POM should not matter, as there shouldn't be any tests actually running in this phase. The new overwite.binaries property that we have added allows us to set the overwriteFilesThatExist configuration setting of the driver-binary-downloader-maven-plugin. By default, it will not overwrite files that already exist. This gives us an option to force the plugin to overwrite existing files if we want to download a new binary version or just refresh our existing binaries.

We have two more configuration settings that are just specifying file paths. The downloadedZipFileDirectory setting is used to specify the file path that will be used to download the binary ZIP files. The rootStandaloneServerDirectory setting is the file path where we extract the driver binaries.

Next, we will use customRepositoryMap to point at a customRepositoryMap.xml. This customRepositoryMap.xml is where download locations for all the binaries we want to download are stored.

Finally, we have added some system properties variables to maven-failsafe-plugin to expose the locations of the binaries when they have been downloaded. driver-binary-downloader-maven-plugin will set a Maven variable that will point to the location of the downloaded binaries. Even though it looks like the variables we are using to set our system properties don't exist, it will be fine.

This is where we have been slightly clever; we have set system properties that Selenium will use automatically to find the location of the driver binaries. This means that we don't need to add any additional code to make things work.

We now need to create RepositoryMap.xml to define the download locations for our binaries; we will probably also need to create the src/test/resources folder since we haven't used it before. The following code has a basic RepositoryMap.xml using the default download locations for the binaries:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<root>
<windows>
<driver id="internetexplorer">
<version id="3.9.0">
<bitrate sixtyfourbit="true">
<filelocation>http://selenium-
release.storage.googleapis.com/3.9/
IEDriverServer_x64_3.9.0.zip</filelocation>
<hash>c9f885b6a339f3f0039d670a23f998868f539e65
</hash>
<hashtype>sha1</hashtype>
</bitrate>
<bitrate thirtytwobit="true">
<filelocation>http://selenium-
release.storage.googleapis.com/3.9/
IEDriverServer_Win32_3.9.0.zip</filelocation>
<hash>dab42d7419599dd311d4fba424398fba2f20e883
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
<driver id="edge">
<version id="5.16299">
<bitrate sixtyfourbit="true" thirtytwobit="true">
<filelocation>https://download.microsoft.com/
download/D/4/1/D417998A-58EE-4EFE-A7CC-
39EF9E020768/MicrosoftWebDriver.exe
</filelocation>
<hash>60c4b6d859ee868ba5aa29c1e5bfa892358e3f96
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
<driver id="googlechrome">
<version id="2.37">
<bitrate thirtytwobit="true" sixtyfourbit="true">
<filelocation>
https://chromedriver.storage.googleapis.com/
2.37/chromedriver_win32.zip</filelocation>
<hash>fe708aac4eeb919a4ce26cf4aa52a2dacc666a2f
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
<driver id="operachromium">
<version id="2.35">
<bitrate sixtyfourbit="true">
<filelocation>https://github.com/operasoftware
/operachromiumdriver/releases/download/v.2.35
/operadriver_win64.zip</filelocation>
<hash>180a876f40dbc9734ebb81a3b6f2be35cadaf0cc
</hash>
<hashtype>sha1</hashtype>
</bitrate>
<bitrate thirtytwobit="true">
<filelocation>https://github.com/operasoftware/
operachromiumdriver/releases/download/v.2.35/
operadriver_win32.zip</filelocation>
<hash>55d43156716d7d1021733c2825e99896fea73815
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
<driver id="marionette">
<version id="0.20.0">
<bitrate sixtyfourbit="true">
<filelocation>
https://github.com/mozilla/geckodriver/
releases/download/v0.20.0/
geckodriver-v0.20.0-win64.zip</filelocation>
<hash>e96a24cf4147d6571449bdd279be65a5e773ba4c
</hash>
<hashtype>sha1</hashtype>
</bitrate>
<bitrate thirtytwobit="true">
<filelocation>
https://github.com/mozilla/geckodriver
/releases/download/v0.20.0/
geckodriver-v0.20.0-win32.zip</filelocation>
<hash>9aa5bbdc68acc93c244a7ba5111a3858d8cbc41d
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
</windows>
<linux>
<driver id="googlechrome">
<version id="2.37">
<bitrate sixtyfourbit="true">
<filelocation>https://chromedriver.storage.
googleapis.com/2.37/
chromedriver_linux64.zip</filelocation>
<hash>b8515d09bb2d533ca3b85174c85cac1e062d04c6
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
<driver id="operachromium">
<version id="2.35">
<bitrate sixtyfourbit="true">
<filelocation>
https://github.com/operasoftware/
operachromiumdriver/releases/download/
v.2.35/operadriver_linux64.zip</filelocation>
<hash>
f75845a7e37e4c1a58c61677a2d6766477a4ced2
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
<driver id="marionette">
<version id="0.20.0">
<bitrate sixtyfourbit="true">
<filelocation>
https://github.com/mozilla/geckodriver/
releases/download/v0.20.0/geckodriver-v0.20.0-
linux64.tar.gz</filelocation>
<hash>
e23a6ae18bec896afe00e445e0152fba9ed92007
</hash>
<hashtype>sha1</hashtype>
</bitrate>
<bitrate thirtytwobit="true">
<filelocation>
https://github.com/mozilla/geckodriver/
releases/download/v0.20.0/geckodriver-v0.20.0-
linux32.tar.gz</filelocation>
<hash>
c80eb7a07ae3fe6eef2f52855007939c4b655a4c
</hash>
<hashtype>sha1</hashtype>
</bitrate>
<bitrate arm="true">
<filelocation>
https://github.com/mozilla/geckodriver/
releases/download/v0.20.0/geckodriver-v0.20.0-
arm7hf.tar.gz</filelocation>
<hash>
2776db97a330c38bb426034d414a01c7bf19cc94
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
</linux>
<osx>
<driver id="googlechrome">
<version id="2.37">
<bitrate sixtyfourbit="true">
<filelocation>
https://chromedriver.storage.googleapis.com/
2.37/chromedriver_mac64.zip</filelocation>
<hash>
714e7abb1a7aeea9a8997b64a356a44fb48f5ef4
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
<driver id="operachromium">
<version id="2.35">
<bitrate sixtyfourbit="true">
<filelocation>
https://github.com/operasoftware/
operachromiumdriver/releases/download/v.2.35/
operadriver_mac64.zip</filelocation>
<hash>
66a88c856b55f6c89ff5d125760d920e0d4db6ff
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
<driver id="marionette">
<version id="0.20.0">
<bitrate thirtytwobit="true" sixtyfourbit="true">
<filelocation>
https://github.com/mozilla/geckodriver/
releases/download/v0.20.0/geckodriver-v0.20.0-
macos.tar.gz</filelocation>
<hash>
87a63f8adc2767332f2eadb24dedff982ac4f902
</hash>
<hashtype>sha1</hashtype>
</bitrate>
</version>
</driver>
</osx>
</root>
This is a big file; it may be easier to copy and paste the latest revision in the driver-binary-downloader README.md on GitHub: https://github.com/Ardesco/selenium-standalone-server-plugin/blob/master/README.md.

If you are on a corporate network that does not allow you to access the outside world, you can of course download the binaries and put them on a local file server. You can then update your RepositoryMap.xml to point at this local file server instead of the internet. This gives you a great deal of flexibility.

Right, let's run our project again to check that everything works; first of all use this code:

mvn clean verify -Dthreads=2 

You will notice that everything worked as normal, despite the fact that we are no longer setting the webdriver.gecko.driver system property on the command line. Next, let's see whether we can now select chrome and have everything still just work by using the following code:

mvn clean verify -Dthreads=2 -Dbrowser=chrome 

This time, you should see two Chrome browsers open up instead of Firefox ones. You may have noticed that the first time you ran this it downloaded a series of binaries, which may have slowed down the first run. This time around though, it had already downloaded them, so it just checked they were there and the test run completed a lot quicker. We no longer have to worry about setting any system properties because this is automatically being done by the plugin modifications we made in our POM file.

We can now give anybody access to our code, and when they check it and run it, things should just work.

Going headless

Going headless seems to be all the rage these days, so let's have a look at how we can add support a headless browser to our burgeoning framework.

It's actually a relatively simple change; first, we are going to use this code to modify our POM to add a <headless> property (we are going to set it to true because you are always going to want to start of running things in headless mode, right?):

<properties>
<project.build.sourceEncoding>UTF-
8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-
8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!-- Dependency versions -->
<selenium.version>3.12.0</selenium.version>
<testng.version>6.14.3</testng.version>
<!-- Plugin versions -->
<driver-binary-downloader-maven-plugin.version>1.0.17
</driver-binary-downloader-maven-plugin.version>
<maven-compiler-plugin.version>3.7.0
</maven-compiler-plugin.version>
<maven-failsafe-plugin.version>2.21.0
</maven-failsafe-plugin.version>
<!-- Configurable variables -->
<threads>1</threads>
<browser>firefox</browser>
<overwrite.binaries>false</overwrite.binaries>
<headless>true</headless>
</properties>

Then, we need to pass that in through maven-failsafe-plugin:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<parallel>methods</parallel>
<threadCount>${threads}</threadCount>
<systemPropertyVariables>
<browser>${browser}</browser>
<headless>${headless}</headless>
<!--Set properties passed in by the driver binary
downloader-->
<webdriver.chrome.driver>${webdriver.chrome.driver}
</webdriver.chrome.driver>
<webdriver.ie.driver>${webdriver.ie.driver}
</webdriver.ie.driver>
<webdriver.opera.driver>${webdriver.opera.driver}
</webdriver.opera.driver>
<webdriver.gecko.driver>${webdriver.gecko.driver}
</webdriver.gecko.driver>
<webdriver.edge.driver>${webdriver.edge.driver}
</webdriver.edge.driver>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>

Finally, we will use this code to update our DriverType enum to read in the new headless system property and apply it to the CHROME and FIREFOX entries:

package com.masteringselenium.config;

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
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.ie.InternetExplorerDriver;
import org.openqa.selenium.ie.InternetExplorerOptions;
import org.openqa.selenium.opera.OperaDriver;
import org.openqa.selenium.opera.OperaOptions;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.safari.SafariOptions;

import java.util.HashMap;

public enum DriverType implements DriverSetup {


FIREFOX {
public RemoteWebDriver getWebDriverObject
(DesiredCapabilities capabilities) {
FirefoxOptions options = new FirefoxOptions();
options.merge(capabilities);
options.setHeadless(HEADLESS);

return new FirefoxDriver(options);
}
},
CHROME {
public RemoteWebDriver getWebDriverObject
(DesiredCapabilities capabilities) {
HashMap<String, Object> chromePreferences =
new HashMap<>();
chromePreferences.put("profile.password_manager_enabled"
, false);

ChromeOptions options = new ChromeOptions();
options.merge(capabilities);
options.setHeadless(HEADLESS);
options.addArguments("--no-default-browser-check");
options.setExperimentalOption("prefs",
chromePreferences);

return new ChromeDriver(options);
}
},
IE {
public RemoteWebDriver getWebDriverObject
(DesiredCapabilities capabilities) {
InternetExplorerOptions options = new
InternetExplorerOptions();
options.merge(capabilities);
options.setCapability(CapabilityType.ForSeleniumServer.
ENSURING_CLEAN_SESSION, true);
options.setCapability(InternetExplorerDriver.
ENABLE_PERSISTENT_HOVERING, true);
options.setCapability(InternetExplorerDriver.
REQUIRE_WINDOW_FOCUS, true);

return new InternetExplorerDriver(options);
}
},
EDGE {
public RemoteWebDriver
getWebDriverObject(DesiredCapabilities
capabilities) {
EdgeOptions options = new EdgeOptions();
options.merge(capabilities);

return new EdgeDriver(options);
}
},
SAFARI {
public RemoteWebDriver getWebDriverObject
(DesiredCapabilities capabilities) {
SafariOptions options = new SafariOptions();
options.merge(capabilities);

return new SafariDriver(options);
}
},
OPERA {
public RemoteWebDriver getWebDriverObject
(DesiredCapabilities capabilities) {
OperaOptions options = new OperaOptions();
options.merge(capabilities);

return new OperaDriver(options);
}
};

public final static boolean HEADLESS =
Boolean.getBoolean("headless");
}

We can now run our project again exactly the same way as before:

mvn clean verify -Dthreads=2

You will see the test start up again, but this time you won't see a browser window pop up. Everything should work exactly as before and the tests should pass as expected. If you want to see the browser window pop up again, you can just set headless to false:

mvn clean verify -Dthreads=2 -Dheadless=false

What happened to GhostDriver?

You may have noticed that I haven't mentioned GhostDriver or PhantomJS at all in the headless section. That's because PhatomJS is no longer under active development and GhostDriver no longer has a core maintainer. PhantomJS is still available and it's possible to get GhostDriver up and running. However, you are then testing with these issues:

  • An out-of-date rendering engine (an old version of QTWebkit)
  • A JavaScript engine that is not used in any of the major browsers
  • A tool that is not completely thread safe

With the release of headless modes from ChromeDriver and FirefoxDriver, it just doesn't make sense to keep using PhantomJS. It was great in its heyday, but it just not a useful tool to use with Selenium any more.

Summary

This chapter should have taught you about how to set up a basic project using Maven to download your dependencies, configure your classpath, and build your code. You will be able to run your tests in parallel with multiple instances of the same browser in TestNG, as well as automatically downloading driver binaries using a Maven plugin to make your test code very portable. You should know how to determine the correct number of threads to use in your tests, as well as overriding this if required.  Finally you will have learned how to run Firefox and Chrome in headless mode so that you can run your tests without interruption locally, and without a desktop environment on CI servers.

In the next chapter, we are going to have a look at how to cope when things go wrong. We will also examine how we can keep track of things, now that we have lots of tests all running at the same time.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Understand the power, simplicity, and limitations of the core Selenium framework
  • Write clear, readable, and reliable tests that perform complex test automation tasks
  • Work with ChromeDriver and GeckoDriver in headless mode

Description

The second edition of Mastering Selenium 3.0 WebDriver starts by showing you how to build your own Selenium framework with Maven. You'll then look at how you can solve the difficult problems that you will undoubtedly come across as you start using Selenium in an enterprise environment and learn how to produce the right feedback when failing. Next, you’ll explore common exceptions that you will come across as you use Selenium, the root causes of these exceptions, and how to fix them. Along the way, you’ll use Advanced User Interactions APIs, running any JavaScript you need through Selenium; and learn how to quickly spin up a Selenium Grid using Docker containers. In the concluding chapters, you‘ll work through a series of scenarios that demonstrate how to extend Selenium to work with external libraries and applications so that you can be sure you are using the right tool for the job.

Who is this book for?

If you are a software tester or a developer with working experience in Selenium and competency with Java, who is interested in automation and are looking forward to taking the next step in their learning journey, then this is the book for you.

What you will learn

  • Provide fast, useful feedback with screenshots
  • Create extensible, well-composed page objects
  • Utilize ChromeDriver and GeckoDriver in headless mode
  • Leverage the full power of Advanced User Interactions APIs
  • Use JavascriptExecutor to execute JavaScript snippets in the browser through Selenium
  • Build user interaction into your test script using JavascriptExecutor
  • Learn the basics of working with Appium

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Jun 29, 2018
Length: 376 pages
Edition : 2nd
Language : English
ISBN-13 : 9781788293686
Vendor :
Apache
Category :
Languages :
Tools :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
OR
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Product Details

Publication date : Jun 29, 2018
Length: 376 pages
Edition : 2nd
Language : English
ISBN-13 : 9781788293686
Vendor :
Apache
Category :
Languages :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
$199.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just Mex$85 each
Feature tick icon Exclusive print discounts
$279.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just Mex$85 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total Mex$ 2,912.97
Mastering Selenium WebDriver 3.0
Mex$1004.99
Selenium WebDriver 3 Practical Guide
Mex$902.99
Selenium Framework Design in Data-Driven Testing
Mex$1004.99
Total Mex$ 2,912.97 Stars icon
Banner background image

Table of Contents

11 Chapters
Creating a Fast Feedback Loop Chevron down icon Chevron up icon
Producing the Right Feedback When Failing Chevron down icon Chevron up icon
Exceptions Are Actually Oracles Chevron down icon Chevron up icon
The Waiting Game Chevron down icon Chevron up icon
Working with Effective Page Objects Chevron down icon Chevron up icon
Utilizing the Advanced User Interactions API Chevron down icon Chevron up icon
JavaScript Execution with Selenium Chevron down icon Chevron up icon
Keeping It Real Chevron down icon Chevron up icon
Hooking Docker into Selenium Chevron down icon Chevron up icon
Selenium – the Future Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Rating distribution
Full star icon Full star icon Full star icon Half star icon Empty star icon 3.3
(3 Ratings)
5 star 33.3%
4 star 33.3%
3 star 0%
2 star 0%
1 star 33.3%
Sashank Mar 15, 2019
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Yes
Amazon Verified review Amazon
Jsh May 31, 2021
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
Clearly written by somebody who has done significant work with Selenium and to a lesser extent Appium, this is a book that contains more useful information and code than any other Selenium book that I had read so far (and I’ve read quite a few).The writing is opinionated, which works well given the experience of the author, but with one exception – unnecessary references to China. I would have expected any decent editor to have had that section removed.For the most part, I agree with the technical advice given, although not quite 100% of the time. There are also some topics missed that I would have included, but those topics are also missed in pretty much every Selenium book. With that caveat, the list of topics covered is still very good. Whether it’s suitable for somebody completely new to automated testing or DevOps, I’m not sure. It’s more an upper-end of intermediate level book IMHO. Aimed at those working in Java, the code is easy to map into other languages (C#, Python etc).Given the rate of change in computing (esp. browsers, mobile app development, virtualisation etc), it’s inevitable that the text is already (I’m writing this at end of May 2021) in need to an update. However, even without that update, it’s still very useful.Recommended, but with caveats mentioned above.
Amazon Verified review Amazon
Jeff Nyman Sep 29, 2018
Full star icon Empty star icon Empty star icon Empty star icon Empty star icon 1
This is, unfortunately, a terrible book. Just in the first two chapters alone, you will find inconsistent code examples (some are clearly bits from the first edition, as you can tell by certain incorrect method signatures), code that doesn't match up with what's being described in the text, and code printed in the book that doesn't match the code in the GitHub repository provided.I submitted errata for the first edition of this book and while there have been some improvements (for example the JUnit inclusion), there are still a series of errors that were not rectified and the introduction of entire new ones.If you're already fairly proficient with Selenium in a Java context, you will likely be able to work your way around these problems. And if you do, you will find that the author does actually create a fairly concise and useful framework that allows you to explore Selenium within Java. This includes up-to-date material such as Chrome and Firefox headless and the use of the new Options approach that Selenium uses, which merges in DesiredCapabilities to a set of options.So there is good to be had here, in terms of some of the material. The problem is that the errors are glaring to the point of ridiculousness, especially in a second edition. You will encounter many such errors just in the first two chapters alone. All of these are certainly surmountable but for someone who is learning, this book will likely rapidly become tedious to the point of distraction.What's most aggravating is that I was able to find these problems on my first read-through of the book. I literally wrote this review after going through the first four chapters. If I could find these errors that quickly, why couldn't whomever reviewed the book? (Assuming, of course, that someone did.) This is an extremely disappointing second attempt by this author and I would highly recommend people avoid this. If you want to get the basics of what the author was providing, simply look up the repo within Packt's space on GitHub. You might not save yourself any aggravation but you will certainly save yourself some money.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.