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. SettingENABLE_PERSISTENT_HOVERING
andrequireWindowFocus
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.