DEV Community

Elias Nogueira
Elias Nogueira

Posted on

The best way to create browser instances using the Factory Pattern, Java, and Selenium WebDriver

Introduction

This article belongs to a series of 3 posts explaining how to use the Factory Pattern to create different browser instances for either local or remote execution using Selenium WebDriver.

You can find the following previous article:

This one will refer to some explanations and code from the previous one, but don’t be afraid, you will understand everything.

General explanation

Image description

Can you imagine you can implement a good approach to create multiple browser instances with only two classes?

The BrowserFactory and the power of enums

We already know that the Enum Types have constant and can have methods.

The BrowserFactory class is an enum with the list of supported browsers (constants) for the implementation I’ve created: Chrome, Firefox, Edge, and Safari.

We can add an extra function on this enum with methods, but instead of creating the regular ones we will make each constant implement two methods: createDriver() for the local execution and getOptions() for the remote execution. In order to do so, we must implement abstract methods inside the enum. Take a look at the example:

public enum BrowserFactory {

    CHROME {
        @Override
        public WebDriver createDriver() {
            WebDriverManager.getInstance(DriverManagerType.CHROME).setup();

            return new ChromeDriver(getOptions());
        }

        @Override
        public ChromeOptions getOptions() {
            ChromeOptions chromeOptions = new ChromeOptions();
            chromeOptions.addArguments("--start-maximized");
            chromeOptions.addArguments("--disable-infobars");
            chromeOptions.addArguments("--disable-notifications");
            chromeOptions.setHeadless(configuration().headless());

            return chromeOptions;
        }
    };

    public abstract WebDriver createDriver();
    public abstract AbstractDriverOptions<?> getOptions();
}
Enter fullscreen mode Exit fullscreen mode

On lines 23 and 24 we have two abstract methods. Each enum constant needs to implement the abstract method.

On lines 5 to 9, there’s the implementation of the createDriver() method creating a local browser instance using the WebDriverManager library. Note that the new browser instance is using the getOptions() and I will explain it in a minute.

You can see that line 24 has the AbstractDriverOptions with an unknown generic, placed with ? as the wildcard. It returns this class because each internal Selenium WebDriver remote browser implementation extends this class, making it easier to set the correct object for the remote instance creation. You can see this by accessing the AbstractDriverOptions Javadoc in the section “Direct Known Subclasses“

Lines 12 to 20 show the specific ChromeOptions we must set for the remote execution, as the remote class is called “option” which must be set.

And why we have the getOptions() method being used in the createDriver()? To automatically set any option we want to. The option classes do not need the browser driver set as needed in the local execution.

The full BrowserDriver implementation can be found at https://github.com/eliasnogueira/selenium-java-browser-factory/blob/master/src/main/java/com/eliasnogueira/driver/BrowserFactory.java

The DriverFactory class

This class is responsible to manage the local or remote instances because we need different code approaches using Selenium WebDriver. We have two main methods: the createInstance() for the local execution and the createRemoveInstance() for the remote execution.

Creating the local instance

The local instance means that we will run the tests locally in the browser installed on the machine.

The method createInstance() has the browser as the method parameter that the BaseTest class will use to determine which browser will be instantiated.

The browser value attribute is transformed in the Target enum and then used in the switch-case.

public class DriverFactory { 

  public WebDriver createInstance(String browser) {
        Target target = Target.valueOf(configuration().target().toUpperCase());
        WebDriver webdriver;

        switch (target) {
            case LOCAL:
                webdriver = BrowserFactory.valueOf(browser.toUpperCase()).createDriver();
                break;
            case REMOTE:
                webdriver = createRemoteInstance(BrowserFactory.valueOf(browser.toUpperCase()).getOptions());
                break;
            default:
                throw new TargetNotValidException(target.toString());
        }
        return webdriver;
    }
}
Enter fullscreen mode Exit fullscreen mode

When the target is LOCAL the BrowserFactory class will call the createDriver() method for the browser we have set. This approach removes the necessity to have the switch-case for each browser as the browser value will be transformed in the enum constant. Less code, more readable code!

You can see this happening on lines 8 to 10.

Remember that the browser to use locally is set in the general.properties class.

Creating the Remote instance

It works similar to the local approach when the code determines if the execution is local or remote. Line 12, in the previous code snippet, shows that the BrowserFactory will call the getOptions() method based on the browser we have set. With this, we have the browser options object. Now it’s time to create the real remote instance.

private RemoteWebDriver createRemoteInstance(MutableCapabilities capability) {
    RemoteWebDriver remoteWebDriver = null;
    try {
        String gridURL = String.format("http://%s:%s", configuration().gridUrl(), configuration().gridPort());

        remoteWebDriver = new RemoteWebDriver(new URL(gridURL), capability);
    } catch (java.net.MalformedURLException e) {
        logger.log(Level.SEVERE, "Grid URL is invalid or Grid is not available");
        logger.log(Level.SEVERE, String.format("Browser: %s", capability.getBrowserName()), e);
    } catch (IllegalArgumentException e) {
        logger.log(Level.SEVERE, String.format("Browser %s is not valid or recognized", capability.getBrowserName()), e);
    }

    return remoteWebDriver;
}
Enter fullscreen mode Exit fullscreen mode

The createRemoteInstance() method expects a MutableCapability object, which is the same as the browser option object because the AbstractDriverOptions extends the MutableCapabilities class.

To create a remote browser instance we must use the RemoteWebDriver class, adding as a parameter the target remote machine (or a grid) and the capability, which is the browser with the options.

On line 4 the URL for the remote machine is created based on the information we have in the grid.properties file. On line 6 the remote instance is created. The rest of the code is the exception handler, necessary to know what happened if any problem occurs during the remote instance creation.

How to associate this with the test?

First, it’s a good practice to have a BaseTest class managing the general pre and post-conditions. The BaseWeb class has the pre-conditions, as a JUnit @BeforeEach annotation, the following:

  • Line 8: getting the configurations, which means all the information from the properties file
  • Line 10: the creation of the browser instance based on the browser set in the properties file
  • Line 11: setting the URL to start the test
public class BaseWeb {

    protected WebDriver driver;
    protected Configuration configuration;

    @BeforeEach
    public void preCondition() {
        configuration = configuration();

        driver = new DriverFactory().createInstance(configuration().browser());
        driver.get(configuration().url());
    }

    @AfterEach
    public void postCondition() {
        driver.quit();
    }
}
Enter fullscreen mode Exit fullscreen mode

The main part related to this approach is in line 10, where the DriverFactory class will know, internally, if the execution is local or remote based on the value set for the target property set in the general.properties file. The browser was also set in this file but through the browser property will be used to create the local browser instance or the remote instance.

The tests must extends the BaseWeb to work properly.

class BookRoomWebTest extends BaseWeb {

    @Test
    void bookARoom() {
        driver.navigate().to(configuration.url() +
                "how-to-create-lean-test-automation-architecture-for-web-using-java-libraries");

        assertThat(driver.findElement(By.cssSelector(".hestia-title.title-in-content")).getText()).
                isEqualTo("How to create Lean Test Automation Architecture for Web using Java libraries");
    }
}
Enter fullscreen mode Exit fullscreen mode

How to run this project in remote machines

If you would like to give it a try the complete project selenium-java-browser-factory has a docker-compose file that will start a Selenium 4 Grid with Chrome, Firefox, and Edge browsers. So you will be able to run remote instances.

To exercise this, please follow these steps:

  1. Start Docker
  2. Navigate, through your Command-Line/Terminal app to the project root folder
  3. Run docker-compose up command
  4. Access http://localhost:4444 to see the Selenium Grid page with the browsers available (Chrome, Edge, and Firefox)
  5. In the general.properties file, change the target attribute value to remote
    • the grid.properties file in src/main/java/resources already have the values to connect to a local grid
  6. Run the BookARoomWebTest
    • you can go to the Selenium Grid dashboard and click on the Sessions tab
    • as soon as the test starts a session related to the browser in use will be listed

The complete example

You can navigate to the following GitHub repo to see a complete and functional example in the branch local-remote-example. Spare some time to look at the new classes added and understand how it was structured.

https://github.com/eliasnogueira/selenium-java-browser-factory/tree/basic-example

Top comments (0)