Book Image

Mastering Selenium WebDriver 3.0 - Second Edition

Book Image

Mastering Selenium WebDriver 3.0 - Second Edition

Overview of 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.
Table of Contents (15 chapters)

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.