How to use the Factory design pattern to create browser instances: local and remote approach
Tags: java, testing, programming, tutorial
Published: 2022-06-20 14:53:15

# Introduction

In the previous article [How to use the Factory design pattern to create browser instances: the simple approach
(http://www.eliasnogueira.com/how-to-use-the-factory-design-pattern-to-create-browser-instances-the-simple-approach/), you have learned how to implement it. I’ve named it “simple” because the code created runs only locally, which is the simplest and most elegant way of using Java to do it.

# Code explanation

To make this post/tutorial as short as possible I will refer, through links, to the existing code implementation where you can access and understand more about the solution applied.

# Difference between local and remote execution

**Local execution** means the usage of the browser installed on the same machine you will run the tests, so we need the web browser and the browser drivers.

**Remote execution** means the execution of the tests in a different machine from the one you will trigger. This “machine” is actually any resource as a different real machine (bare-metal), a Docker container, or a remote service (E.g: SauceLabs, BrowserStack).

## The technical differences in Selenium

In the **local execution**, we directly use the browser instance. Don’t forget you need also the browser driver and it can be easily configured using the [WebDriverManager](The technical differences in Selenium
In the local execution, we directly use the browser instance. Don’t forget you need also the browser driver and it can be easily configured using the WebDriverManager library.) library.

```java
// setting up the browser driver with the WebDriverManager library
WebDriverManager.getInstance(CHROME).setup();

// creating the local browser instance
WebDriver driver = new ChromeDriver();
```

The WebDriverManager will set up the Chrome driver and Selenium will look a the local Google Chrome installation and will use it to run your tests.

But, for the **remote execution**, things are a little bit different. You will need to:

* have the target machine URL
* use the [RemoteWebDriver](https://www.selenium.dev/documentation/webdriver/remote_webdriver/) class, instead of the browser class with the following parameters:
  * the URL for the target machine
  * the “capability” to run


# Code organization

Similar to the [How to use the Factory design pattern to create browser instances: the simple approach](http://www.eliasnogueira.com/how-to-use-the-factory-design-pattern-to-create-browser-instances-the-simple-approach/) article shows how to create a factory class to manage different browsers to run locally, we have now to create a way to determine when to run locally and when to run in a remote machine.

This is the class diagram that we will work on.

![Class diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y7vcieb0daqwi9n6puwh.png)

## Diagram explanation

### manager package

The `IDriveManager` is an interface that has the methods to implement in the manager classes. The manager classes have the implementation for the local, and now, remote execution.
The local execution is done by the method `createDriver()` and the remote execution is done by the `getOptions()` method. I will explain more about it later.

### driver package

The `DriverFactory` class is the one responsible to know if the execution will be either local or remote. We manage this through a property value. Based on the target environment we will instantiate either the `LocalDriverFactory` or the `RemoteDriverFactory`. This class implemented the interface `IDriverFactory`.

### factory package

The LocalDriverFactory class is responsible to create the local browser instance using the `createDriver()` method from the manager classes.

The `RemoteDriverFactory` class is responsible to create the remote browser execution using the `getOptions()` from the manager classes. The internal private method `createRemoteInstance()` will connect to the remote machine before the creation of the browser instance.

# Implementation

I will explain here how to evolve code based on the simple approach. The following sub-items will explain it.

## 1. Manager the local and remote execution

### New properties
The first thing we need to create is a way to run the test using either a local or remote machine. For the remote execution, we will need to parameterize the URL and port we will connect our tests. We also need to add a way to say when we need to execute it as local or remote.

The approach I’ve created is to add a new [grid.properties](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/resources/grid.properties) file to add the Selenium Grid URL and port

```
# grid url and port
grid.url = localhost
grid.port = 4444
```

> Note: the file is named grid, as well as the properties because the way to run Selenium tests in different machines implies the Selenium Grid features.

We also need to update the [general.properties](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/test/resources/general.properties) file to have the local property to tell, somehow, we can execute it locally or remote. Line 2 shows the new property.

Finally, in the [Configuration class](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/config/Configuration.java#L35-L53), you will see the new association to the new properties created in the `gird.properties` and `general.properties`. Also the load of the grid.properties in the `@Config.Sources` annotation from the Owner library.

### New class to manage the local and remote execution

The previous DriverFactory was managing the local execution. Now, to maintain a better naming consistency, we are changing it to manage the execution into local or remote. It has a simple switch-case to instantiate the correct internal class.

```java
public class DriverFactory implements IDriverFactory {

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

        switch (target) {
            case LOCAL:
                webdriver = new LocalDriverFactory().createInstance(browser);
                break;
            case REMOTE:
                webdriver = new RemoteDriverFactory().createInstance(browser);
                break;
            default:
                throw new TargetNotValidException(target.toString());
        }
        return webdriver;
    }

    enum Target {
        LOCAL, REMOTE
    }
}
```

Lines 21 to 23 show an internal enum named `Target` which has `LOCAL` and `REMOTE` as possible values.

Line 5 loads the target property from the `general.properties` file and converts it to `Target` enum, whereas line 8 applies a switch statement in the enum.

When the execution is from the local, the [LocalDriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/LocalDriverFactory.java) class is in use to create the `WebDriver` instance based on the browser set in `general.properties` file. When the execution is remote the [RemoteDriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/RemoteDriverFactory.java) class is in use. The `LocalDriverFactory` class is the same as the [previous DriverFactory from the basic-example branch](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/basic-example/src/main/java/com/eliasnogueira/driver/factory/DriverFactory.java).

### New LocalDriverFactory

This class is identical to the previous [DriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/basic-example/src/main/java/com/eliasnogueira/driver/factory/DriverFactory.java). You can take a look at the implemented [LocalDriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/LocalDriverFactory.java) class.

### New RemoteDriverFactory class

We now have a new class to instantiate the different browsers for the remote execution. I will assume here you are already familiar with Selenium Grid, so I will skip the explanation of the code that creates the `RemoteWebDriverInstance`. If you need to learn more about it, please read the official [Selenium Grid documentation](https://www.selenium.dev/documentation/grid/) and the [code I’ve implemented](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/RemoteDriverFactory.java#L72-L86) for this solution.

Basically, we will have the same `switch-case` strategy for each browser compared to the [LocalDriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/LocalDriverFactory.java), but instead of instantiating the local browser, we need to instantiate the remote one.

```java
@Override
public WebDriver createInstance(String browser) {
    MutableCapabilities capability;
    BrowserList browserToCreate = BrowserList.valueOf(browser.toUpperCase());

    switch (browserToCreate) {
        case CHROME:
            capability = new ChromeDriverManager().getOptions();
            break;
        case FIREFOX:
            capability = new FirefoxDriverManager().getOptions();
            break;
        case EDGE:
            capability = new EdgeDriverManager().getOptions();
            break;
        case SAFARI:
            capability = new SafariDriverManager().getOptions();
            break;
        default:
            throw new BrowserNotSupportedException(browser + "is not supported!");
    }

    return createRemoteInstance(capability);
}
```

Note that the [RemoteDriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/RemoteDriverFactory.java#L47-L70) class is now using the `getOptions()` method instead of the `createDriver()` from each [manager driver class](https://github.com/eliasnogueira/selenium-java-browser-factory/tree/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/manager).

## 2. Additional getOptions() method in the driver manager classes

Each driver manager class will have, additionally, a new method to use when the execution is remote. As the `RemoteWebDriver` does not accept a browser instance directly, but a [MutableCapabilities](https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/MutableCapabilities.html) class, we need to specify the correlated option class of the browser.

Each browser has its own `*Options` class, like [ChromeOptions](https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/chrome/ChromeOptions.html), [FirefoxOptions](https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/firefox/FirefoxOptions.html), and so on. Those classes exist for the remote execution and have pre-configured options as their identification plus methods that can extend their features. Internally the `*Options` classes have a hierarchy dependency on the `MutableCapabilities` class.

What we need to do is to add the `getOptions()` methods to each `DriverManager` class. This method must instantiate the related browser option and can add more features.

```java
@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;
}
```

The code above shows the `getOptions()` method for the [ChromeDriverManager](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/manager/ChromeDriverManager.java) class where we are:
 * creating an instance of its option on line 3
 * adding additional arguments to the browser on lines 4 to 6
 * setting it as headless based on the headless configuration value present in the `general.properties`
 * returning the object instantiated to be used as the `RemoteWebDriver`

You can see all the implemented `DriverManager` classes in the [manager package](https://github.com/eliasnogueira/selenium-java-browser-factory/tree/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/manager).

# How to run

The execution to local or remote will is based on the `target` property value in the [general.properties](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/test/resources/general.properties#L2) file. We know that when the execution is `local` the [DriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/DriverFactory.java) will use the [LocalDriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/LocalDriverFactory.java) class to create the local browser instance and when the execution is `remote` the [DriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/DriverFactory.java) will use the [RemoteDriverFactory](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/src/main/java/com/eliasnogueira/driver/factory/RemoteDriverFactory.java) to create a remote browser instance.

## Running it locally

Follow these steps:

1. Open the `general.properties` file located in `src/test/java/resources`
2. Change the `target` property value to `local`
3. Change the `browser` property value to any browser you have installed
4. Run the `BookARoomWebTest` class

The expected result is the test being executed in your local machine for your set browser. This is the same way we did in the previous post.

## Running it in a remote machine

Now things get more interesting. We need to find a way to execute the test on a remote machine. As written before a remote machine can be a physical or virtual machine existent in your network or a cloud provider. To be able to run it on your machine right now the Docker is one of your best choices.

The project has a [docker-compose.yml](https://github.com/eliasnogueira/selenium-java-browser-factory/blob/local-remote-example/docker-compose.yml) file which will start a full Selenium Grid infrastructure containing the sessions, distributors, router, event bus, and the browsers. You can learn more about the Selenium Grid 4 on their [official page](https://www.selenium.dev/documentation/grid/) and in their [GitHub project](https://github.com/SeleniumHQ/docker-selenium).

As a pre-condition, you need to have Docker installed on your machine and then 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