Mastering Selenium WebDriver 3.0 - Second Edition

4 (3 reviews total)
By Mark Collin
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Creating a Fast Feedback Loop

About this book

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.

Publication date:
June 2018
Publisher
Packt
Pages
376
ISBN
9781788299671

 

Chapter 1. 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 2, Working 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.

Note

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.

Note

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 geckodriverbinary; 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.

Note

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?

Note

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.

Note

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.

Note

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.

Note

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>

Note

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

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>

Note

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.

Note

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.

Note

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. 

Note

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.

Note

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>

Note

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.

About the Author

  • Mark Collin

    Mark Collin has been working in the software industry since 2001. He started his career in the financial sector before moving into consultancy. He has an eclectic range of skills and proficiencies, which include test automation, security and penetration testing, and performance testing. Mark is the creator and maintainer of driver-binary-downloader maven-plugin, and the Query library used in this book. He is also a core contributor to jmeter-maven-plugin, a tool that allows you to run JMeter tests through Maven. He has also contributed code to the core Selenium code base.

    Browse publications by this author

Latest Reviews

(3 reviews total)
I really liked that the book is useful for learning the automation process with Java and Selenium
This book is great value.
I am disappointed, I expected this book to be different than the author's previous book, but it was almost a copy of the previous one.

Recommended For You

Book Title
Access this book, plus 7,500 other titles for FREE
Access now