Setting up a local Selenium Grid using Docker and .NET Core

Posted on Monday, 20th March 2017

Since jumping on the Docker bandwagon I’ve found its utility spans beyond the repeatable deployments and consistent runtime environment benefits that come with the use of containers. There’s a whole host of tooling and use cases emerging which take advantage of containerisation technology, one use case that I recently discovered after a conversation with Phil Jones via Twitter is the ability to quickly set up a Selenium Grid.

Setting up and configuring a Selenium Grid has never been an simple process, but thanks to Docker it’s suddenly got a whole lot easier. In addition, you’re now able to run your own Selenium Grid locally and greatly speed up your tests’ execution. If that isn’t enough then another benefit is because the tests execute inside of a Docker container, you’ll no longer be blocked by your browser navigating the website you’re testing!

Let’s take a look at how this can be done.

Note: For the impatient, I’ve put together a working example of the following post in a GitHub repository you can clone and run.

Selenium Grid Docker Compose file

For those that haven’t touched Docker Compose (or Docker for that matter), a Docker Compose file is a Yaml based configuration document (often named docker-compose.yml) that allows you to configure your applications’ Docker environment.

Without Docker Compose you’d need to manually run your individual Dockerfile files specifying their network connections and configuration parameters along the way. With Docker Compose you can configure everything in a single file and start your environment with a simple docker-compose up command.

Below is the Selenium Grid Docker Compose configuration you can copy and paste:

# docker-compose.yml

version: '2'
services:
    selenium_hub:
        image: selenium/hub:3.0.1-aluminum
        container_name: selenium_hub
        privileged: true
        ports:
            - 4444:4444
        environment:
            - GRID_TIMEOUT=120000
            - GRID_BROWSER_TIMEOUT=120000
        networks:
            - selenium_grid_internal

    nodechrome1:
        image: selenium/node-chrome-debug:3.0.1-aluminum
        privileged: true
        depends_on:
            - selenium_hub
        ports:
            - 5900
        environment:
            - no_proxy=localhost
            - TZ=Europe/London
            - HUB_PORT_4444_TCP_ADDR=selenium_hub
            - HUB_PORT_4444_TCP_PORT=4444
        networks:
            - selenium_grid_internal

    nodechrome2:
        image: selenium/node-chrome-debug:3.0.1-aluminum
        privileged: true
        depends_on:
            - selenium_hub
        ports:
            - 5900
        environment:
            - no_proxy=localhost
            - TZ=Europe/London
            - HUB_PORT_4444_TCP_ADDR=selenium_hub
            - HUB_PORT_4444_TCP_PORT=4444
        networks:
            - selenium_grid_internal

networks:
    selenium_grid_internal:

In the above Docker Compose file we’ve defined our Selenium Hub (selenium_hub) service, exposing it on port 4444 and attaching it to a custom network named selenium_grid_internal (which you’ll see all of our nodes are on).

selenium_hub:
    image: selenium/hub:3.0.1-aluminum
    container_name: selenium_hub
    privileged: true
    ports:
        - 4444:4444
    environment:
        - GRID_TIMEOUT=120000
        - GRID_BROWSER_TIMEOUT=120000
    networks:
        - selenium_grid_internal

All that’s remaining at this point is to add our individual nodes. In this instance I’ve added two Chrome based nodes, named nodechrome1 and nodechrome2:

nodechrome1:
    image: selenium/node-chrome-debug:3.0.1-aluminum
    privileged: true
    depends_on:
        - selenium_hub
    ports:
        - 5900
    environment:
        - no_proxy=localhost
        - TZ=Europe/London
        - HUB_PORT_4444_TCP_ADDR=selenium_hub
        - HUB_PORT_4444_TCP_PORT=4444
    networks:
        - selenium_grid_internal

nodechrome2:
    image: selenium/node-chrome-debug:3.0.1-aluminum
    ...

Note: If you wanted to add Firefox to the mix then you can replace the image: value with the following Docker image:

nodefirefox1:
    image: selenium/node-firefox-debug:3.0.1-aluminum
    ...

Now if we run docker-compose up you’ll see our Selenium Grid environment will spring into action.

To verify everything is working correctly we can navigate to http://0.0.0.0:4444 in our browser where we should be greeted with the following page:

Connecting Selenium Grid from .NET Core

At the time of writing this post the official Selenium NuGet package does not support .NET Standard, however there’s a pending pull request which adds support (the pull request has been on hold for a while as the Selenium team wanted to wait for the tooling to stabilise). In the mean time the developer that added support released it as a separate NuGet package which can be downloaded here.

Alternatively just create the following .csproj file and run the dotnet restore CLI command.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0-preview-20170123-02" />
    <PackageReference Include="xunit" Version="2.2.0-beta5-build3474" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0-beta5-build1225" />
    <PackageReference Include="CoreCompat.Selenium.WebDriver" Version="3.2.0-beta003" />
  </ItemGroup>

</Project>

Next we’ll create the following base class that will create a remote connection to our Selenium Grid:

public abstract class BaseTest
{
    private IWebDriver _driver;

    public IWebDriver GetDriver()
    {
        var capability = DesiredCapabilities.Chrome();
        if (_driver == null){
            _driver = new RemoteWebDriver(new Uri("http://0.0.0.0:4444/wd/hub/"), capability, TimeSpan.FromSeconds(600));
        }

        return _driver;
    }
}

After that we’ll create a very simple (and trivial) test that checks for the existence of an ID on google.co.uk.

public class UnitTest1 : BaseTest 
{

    [Fact]
    public void TestForId()
    {
        using (var driver = GetDriver())
        {
            driver.Navigate().GoToUrl("http://www.google.co.uk");
            var element = driver.FindElement(By.Id("lst-ib"));
            Assert.True(element != null);
        }
    }

    ...

}

Now if we run our test (either via the dotnet test CLI command or from your editor or choice) we should see our Docker terminal console showing our Selenium Grid container jump into action as it starts executing the test one one of the registered Selenium Grid nodes.

At the moment we’re only executing the one test so you’ll only see one node running the test, but as you start to add more tests across multiple classes the Selenium Grid hub will start to distribute those tests across its cluster of nodes, dramatically increasing your test execution time.

If you’d like to give this a try then I’ve added all of the source code and Docker Compose file in a GitHub repository that you can clone and run.

The drawbacks

Before closing there are a few drawbacks to this method of running tests, especially if you’re planning on doing it locally (instead of setting a grid up on a Virtual Machine via Docker).

Debugging is made harder
If you’re planning on using Selenium Grid locally then you’ll lose the visibility of what’s happening in the browser as the tests are running within a Docker container. This means that in order to see the state of the web page on a failing test you’ll need to switch to local execution using the Chrome / FireFox or Internet Explorer driver.

Reaching localhost from within a container
In this example we’re executing the tests against an external domain (google.co.uk) that our container can resolve. However if you’re planning on running tests against a local development environment then there will be some additional Docker configuration required to allow the container to access the Docker host’s IP address.

Conclusion

Hopefully this post has broadened your options around Selenium based testing and demonstrated how pervasive Docker is becoming. I’m confident that the more Docker (and other container technology for that matter) matures, the more we’ll see said technology being used for such use cases as we’ve witnessed in this post.