Driver Factory Part 3 – RemoteWebDrivers and my very own grid

Driver Factory Part 3 – RemoteWebDrivers and my very own grid

This entry is part 1 of 5 in the series Building a WebDriver Grid

So last week I got my code running on a home grid, but did not have time to explain what I did, so here goes…

What is a grid and why would you use it?

If you have pulled and run any of my tests, or tried building up something yourself, after the initial wow factor of watching a browser fire up and start doing things all by itself, you probably quite quickly got bored of having to stop pretty much everything else you were doing whilst the tests run. It’s not such an issue when you have a small project like this, but once it takes 10-15 minutes per run it really gets quite frustrating.

Now whilst there is no substitute for running locally if you can, whilst writing and debugging tests, once it is done, ideally you want it to get on with its own thing and just report back any failures. There are a couple of ways to do this. You could use a headless browser. Traditionally PhantomJS, a browser that was written to run in the background and actually display nothing was used for automated headless testing. Its end as a project has been announced now that Google have announced their work at getting Chrome running in a headless mode. Of course being Chrome this also has the advantage of being a real production browser. that many of your desktop users will be using.

That is a nice quickie to test. Although a search will probably show pages explaining that it will only work in Linux, they are quite old: It seems that this works just fine in Windows with Chrome version 61 and Selenium 3.6.0. (The current versions.) It is simple to have a try. When you want a driver:

final ChromeOptions options = new ChromeOptions()
        .addArguments("--test-type", "--start-maximized", "--headless", "--disable-gpu");
// add additional required options here
this.driver = new ChromeDriver(options);

I have only had a basic play with it, but I tried it earlier today at work, and they gave exactly the same results as when viewed on the screen. I can see that getting usage!

Unfortunately however this still only works for Chrome, and even more importantly, what if I want to test on a different platform? Perhaps I want to run my tests on Windows, but also test Safari, Chrome and Firefox on MacOS. This is where a grid comes in. You set up one machine as a hub. This acts as the connection point for your tests, and when requested will try to find a node (computer or other device) that can provide the type of WebDriver you require. They can be on the same machine, but you can connect nodes from any OS to your hub.

Testing remotely

Sadly I don’t have a Mac these days (I really don’t like OSX/MacOS but it would be cool to have one to play around testing on!) and Linux and mobile will be another day. I do however have a nice little silent HTPC that generally isn’t doing much while I’m working on this that I can set up as a hub. In this case I am also going to use it to provide the nodes as well. This is a training project after all, not production.

To run the hub you need a JDK installed (Java 9 for me as its out now!) and you can download the selenium standalone server from http://www.seleniumhq.org/download/

You will also need to copy the executables from your resources folder to the hub computer as well as installing the browsers you plan to use. I put them all together in one folder. Then to set up the grid, on separate command prompts….

java -jar selenium-server-standalone-3.6.0.jar -role hub

java -Dwebdriver.chrome.driver=chromedriver.exe -jar selenium-server-standalone-3.6.0.jar -port 5555 -role node -hub http://localhost:4444/grid/register -browser "browserName=chrome, platform=WINDOWS, maxInstances=10"

java -Dwebdriver.edge.driver=MicrosoftWebDriver.exe -jar selenium-server-standalone-3.6.0.jar -port 5556 -role node -hub http://localhost:4444/grid/register -browser "browserName=MicrosoftEdge, platform=WINDOWS, maxInstances=10"

java -Dwebdriver.ie.driver=IEDriverServer.exe -jar selenium-server-standalone-3.6.0.jar -port 5557 -role node -hub http://localhost:4444/grid/register -browser "browserName=internet explorer, platform=WINDOWS, maxInstances=10"

java -Dwebdriver.gecko.driver=geckodriver.exe -jar selenium-server-standalone-3.6.0.jar -port 5558 -role node -hub http://localhost:4444/grid/register -browser "browserName=firefox, platform=WINDOWS, maxInstances=10"

I have been told that http://paypal.github.io/SeLion/html/documentation.html#getting-started is far better, and to be fair it is much quicker and easier to set up, but I did not find it as stable in my brief experiments.

Extending the DriverFactory to use the grid

So the constructor I need to fire off a test on the grid is:

this.driver = new RemoteWebDriver(GridUrl, options);

The good news is that this works identically for all browsers so a little tinkering with the AbstractDriverManager

package tech.alexontest.poftutor.infrastructure;

import org.openqa.selenium.WebDriver;

import java.net.MalformedURLException;
import java.net.URL;

public abstract class AbstractDriverManager implements WebDriverManager {
    protected WebDriver driver;
    private URL gridUrl;

    AbstractDriverManager() {
        try {
            gridUrl = new URL("http://192.168.0.17:4445/wd/hub");
        } catch (final MalformedURLException e) {
            e.printStackTrace();
        }
    }

    URL getGridUrl() {
        return gridUrl;
    }

    @Override
    public void quitDriver() {
        if (null != driver) {
            driver.quit();
            System.out.println("WebDriver quit");
            driver = null;
        }
    }

    @Override
    public WebDriver getDriver() {
        if (null == driver) {
            startService();
            createDriver();
        }
        return driver;
    }
}

Then we can make a FirefoxDriverManager as

package tech.alexontest.poftutor.infrastructure;

import org.openqa.selenium.firefox.FirefoxDriverLogLevel;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.firefox.GeckoDriverService;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.io.File;
import java.io.IOException;

public class FirefoxDriverManager extends AbstractDriverManager implements WebDriverManager {
    private GeckoDriverService geckoDriverService;
    private final File geckoDriverExe;

    FirefoxDriverManager() {
        final String path = getClass().getClassLoader().getResource("geckodriver.exe").getPath();
        geckoDriverExe = new File(path);
        System.setProperty("webdriver.gecko.driver", path);
    }

    @Override
    public void startService() {
        if (null == geckoDriverService) {
            try {
                geckoDriverService = new GeckoDriverService.Builder()
                        .usingDriverExecutable(geckoDriverExe)
                        .usingAnyFreePort()
                        .build();
                geckoDriverService.start();
            } catch (final IOException e) {
                e.printStackTrace();
            }
            System.out.println("GeckoDriverService Started");
        }
    }

    @Override
    public void stopService() {
        if (null != geckoDriverService && geckoDriverService.isRunning()) {
            geckoDriverService.stop();
            System.out.println("GeckoDriverService Stopped");
        }
    }

    @Override
    public String createDriver() {
        final FirefoxOptions options = new FirefoxOptions()
                .setLogLevel(FirefoxDriverLogLevel.ERROR); //to stop the debug spam
        // add additional options here as required
        this.driver = new RemoteWebDriver(getGridUrl(), options);
        System.out.println("FirefoxDriver Started");
        return DesiredCapabilities.firefox().getBrowserName();
    }
}

Note for Firefox the option setting to turn off the annoying spam from the driver. We can always remove that if I really do want all the gory details for debugging. Basically all the other driverManagers are identical apart from the appropriate paths, executables, options classes and options applied.

Now we have our driverManagers configured to start the xxxDriverServers we can make a couple of final changes to optimise running. When using the RemoteWebDriver, a new instance of the DriverSevice will not be created if one already exists. This means I can reuse the service in much the same way that I reuse the driver in my AbstractSingleDriverTest class. This works for both The AbstractTest and AbstractSingleDriverTest. e.g.

package tech.alexontest.poftutor.infrastructure;

import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;

/**
 * My default AbstractTest class that creates a new WebDriver of the same type for Each Test.
 * Supports only JUnit 5 tests.
 */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract public class AbstractTest {
    private WebDriver driver;
    private WebDriverManager driverManager;

    @BeforeAll
    void startService() {
        System.out.println("Preparing DriverManager");
        driverManager = DriverManagerFactory.getManager(DriverType.CHROME);
    }

    @BeforeEach
    void setup() {
        System.out.println("Getting Driver");
        driver = driverManager.getDriver();
    }

    @AfterEach
    void teardown() {
        System.out.println("Quitting Driver");
        driverManager.quitDriver();
    }

    @AfterAll
    void stopService() {
        driverManager.stopService();
    }

    protected WebDriver getDriver(final String url) {
        driver.get(url);
        return driver;
    }
}

Of course I have now built a pretty substantial piece of code to create my DriverFactory so I certainly should be testing it. Unfortunately, this being my third test class, it also has a third different requirement for the browser setup requiring another AbstractTest class. Its not ideal that my three test classes all require their own AbstractTest, and I did look at using overrides to fix them, but when you end up overriding (nearly) all the methods, it’s definitely not the way to go about it.

Possibly in this case, supporting multiple Browsers within a single test class I could get away with just building a standalone Test class, but now I SHOULD have most if not all my bases covered to build tests upon.

package tech.alexontest.poftutor.infrastructure;

import org.junit.jupiter.api.AfterEach;
import org.openqa.selenium.WebDriver;

/**
 * Abstract Test class that can use different browsers within a single execution.
 * Used for testing the Framework itself. Supports JUnit 5 tests only.
 */
abstract public class AbstractCrossBrowserTest {
    private WebDriverManager driverManager;

    @AfterEach
    public void teardown() {
        System.out.println("Quitting WebDriver");
        driverManager.quitDriver();
        driverManager.stopService();
    }

    protected WebDriverManager getDriverManager() {
        return driverManager;
    }

    protected void setDriverManager(final WebDriverManager driverManager) {
        this.driverManager = driverManager;
    }

    protected WebDriver getDriver(final String url) {
        final WebDriver driver = driverManager.getDriver();
        driver.get(url);
        return driver;
    }
}

And then here is the test class:

package tech.alexontest.poftutor;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import tech.alexontest.poftutor.infrastructure.AbstractCrossBrowserTest;
import tech.alexontest.poftutor.infrastructure.DriverManagerFactory;
import tech.alexontest.poftutor.infrastructure.DriverType;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

@Tag("Framework")
class DriverFactoryTests extends AbstractCrossBrowserTest{
    private final String homePageURL = "https://alexanderontesting.com/";
    private final String OS = System.getProperty("os.name");

    @Test
    @DisplayName("Chrome Browser Tests can be run on Windows")
    void chromeDriverFactoryWorks() {
        checkThatPageLoads(DriverType.CHROME, "Chrome");
    }

    @Test
    @DisplayName("Edge Browser Tests can be run on Windows 10")
    void edgeDriverFactoryWorks() {
        assumeTrue(OS.equalsIgnoreCase("windows 10"), "Edge only runs on Windows 10.");
        checkThatPageLoads(DriverType.EDGE, "MicrosoftEdge");
    }

    @Test
    @DisplayName("Firefox Browser Tests can be run on Windows")
    void firefoxDriverFactoryWorks() {
        checkThatPageLoads(DriverType.FIREFOX, "Firefox");
    }

    @Test
    @DisplayName("Internet Explorer Browser Tests can be run on Windows")
    void ieDriverFactoryWorks() {
        assumeTrue(OS.contains("Windows"));
        checkThatPageLoads(DriverType.IE, "Internet Explorer");
    }

    private void checkThatPageLoads(final DriverType type, final String browserName) {
        setDriverManager(DriverManagerFactory.getManager(type));
        getDriverManager().startService();
        assertThat(getDriverManager().createDriver())
                .as("Checking That browser is of type %s", browserName)
        .isEqualToIgnoringCase(browserName);
        assertThat(getDriver(homePageURL).getCurrentUrl())
                .isEqualToIgnoringCase(homePageURL);
    }

Of course I can just add extra tests for each new browser I support, and even local browsers when I re-enable local testing. In fact with that thought in mind, I would like to be able to run locally on Chrome when developing, and even headless if for any reason my grid is down. As I already know that I can pass my ChromeDriverService in the constructor of a Chromedriver that should be easy enough to support without messing up anything:

Firstly add to my DriverType enum

CHROME_LOCAL,
CHROME_LOCAL_HEADLESS,

It’s not perfect, I would prefer to handle these options somewhere else, but will do for now. So the additions to permit local Chrome browsers in ChromeDriverManager:

public class ChromeDriverManager extends AbstractDriverManager implements WebDriverManager {

    private ChromeDriverService chromeDriverService;
    private final File chromedriverExe;
    private boolean isLocal;
    private boolean isHeadless;

    ChromeDriverManager(boolean isLocal, boolean isHeadless) {
        final String path = getClass().getClassLoader().getResource("chromedriver.exe").getPath();
        chromedriverExe = new File(path);
        System.setProperty("webdriver.chrome.driver", path);
        this.isLocal = isLocal;
        this.isHeadless = isHeadless;
    }
.
. and
.
    @Override
    public String createDriver() {
        final ChromeOptions options = new ChromeOptions()
                .addArguments("--test-type", "--start-maximized");
        // add additional required options here
        if (!isLocal) {
            driver = new RemoteWebDriver(getGridUrl(), options);
        } else {
            if (isHeadless) {
                options.addArguments("--headless", "--disable-gpu");
            }
            driver = new ChromeDriver(chromeDriverService, options);
            driver.navigate().to(getGridUrl());
        }
        System.out.println("ChromeDriver Started");
        return DesiredCapabilities.chrome().getBrowserName();
    }
}

And the right calls in DriverManagerFactory:

case CHROME:
    driverManager = new ChromeDriverManager(false, false);
    break;

case CHROME_LOCAL:
    driverManager = new ChromeDriverManager(true, false);
    break;

case CHROME_LOCAL_HEADLESS:
    driverManager = new ChromeDriverManager(true, true);
    break;

Add in a couple of new tests in DriverFactoryTests:

@Test
@DisplayName("Chrome Browser Tests can be run locally on screen")
void chromeDriverFactoryWorksLocal() {
    checkThatPageLoads(DriverType.CHROME_LOCAL, "Chrome");
}

@Test
@DisplayName("Chrome Browser Tests can be run locally headless")
void chromeDriverFactoryWorksHeadless() {
    checkThatPageLoads(DriverType.CHROME_LOCAL_HEADLESS, "Chrome");
}

and we are good to go.

DriverFactory Tests all passing

I probably don’t want to run these every time I run my tests. So one last tweak: You may have noticed

@Tag(“Framework”)

on the DriverFactoryTests. I can set a filter in the plugin section of build.gradle to exclude these files normally. I can comment them out or run in the IDE as and when required.

filters {
    tags {
        // Framework tests need to be run only when required to verify the this framework is still working.
        exclude "Framework"
        //include "Navigation", "Content"
    }

Excellent! That’s more than enough for this week so I will leave it there for now. As always, this code is available on GitHub and for the first time I have tried tagging this as a release so you might be able to look back and find it.

 

Progress made this week:

  • Built a Webdriver grid to run my tests on a remote machine.
  • Extended the DriverFactory to be able to control a number of browsers over the grid, and Chrome locally, both on screen and headless.
  • Written a test class for my framework to verify that the supported browser options work.

Lessons learnt:

  • Setting up the grid was remarkably painless and well worth the effort.
  • Debugging comments can swiftly get out of control and either need ruthlessly removing once you have things running properly, or sent to a proper logging framework.

Tempted as I am to try and hook up a mobile to my grid next week I really should heed the warnings that Checkstyle and SonarLint are throwing at me and do something about proper logging.

Alexander

 

Series NavigationAll is not as it seems: Checking the right browser launched >>
Comments are closed.