Write Dockerized End-to-End Tests With JUnit 5 and Selenium WebDriver

It’s hard to configure the environment that is required to run end-to-end tests which use Selenium WebDriver. The problem is that every computer that runs our end-to-end tests must have a desktop environment and we must install the used web browsers on these computers. Also, we must deal with problems caused by incompatible operating systems and web browsers. For example, we cannot write tests which use Microsoft Edge if our build server runs Linux.

It’s clear that using the “manual” approach doesn’t make any sense because it just requires too much work. Luckily, we have another choice: we can write end-to-end tests which use dockerized web browsers. This blog post describes how we can manage the started Docker containers with TestContainers when we are writing end-to-end tests with JUnit 5.

After we have finished this blog post, we: 

Note: This blog post assumes that you are familiar with JUnit 5.

Let’s start by taking a quick look at TestContainers.

A Quick Introduction to TestContainers

TestContainers is a library that allows us to start a Docker container before our test methods are run and stop it after our test methods have been run. At the moment, the prerequisites of the TestContainers library are:

Note: Before you install Docker, you should take a look at the general Docker requirements of the TestContainers library. This document identifies the supported Docker versions and lists the known issues of each environment.

TestContainers provides a quite versatile support for different Docker images. For example, one quite common use case is to use a dockerized database which helps us to write integration tests for code that uses a database.

Also, TestContainers allows us to start and stop dockerized web browsers, and this blog post describes how we can write simple end-to-end tests which control the Chrome web browser by using a remote WebDriver.

Next, we will find out how we can get the required dependencies with Maven and Gradle.

Getting the Required Dependencies

Our example requires both compile time and runtime dependencies. These dependencies are described in the following:

First, our example requires the following compile time dependencies:

  • The junit-jupiter-api dependency (version 5.4.0) provides the public API for writing tests and extensions which use JUnit 5.
  • The assertj-core (version 3.12.0) is an optional dependency that allows us to write assertions by using its fluent API. If we want to use the “standard” assertion API of JUnit 5, we don’t need this dependency.
  • The TestContainers Core dependency (version 1.10.6) contains the core functionality of the TestContainers library, and provides support for generic containers and Docker Compose.
  • The TestContainers JUnit Jupiter dependency (version 1.10.6) provides support for JUnit 5.
  • The TestContainers Selenium dependency (version 1.10.6) provides support for running WebDriver containers.
  • The selenium-api dependency (version 3.141.59) allows us to write tests which use Selenium WebDriver.
  • The selenium-chrome-driver dependency (version 3.141.59) allows us to control the Chrome web browser by using Selenium WebDriver.

Second, our example requires the following runtime dependencies:

  • The junit-jupiter-engine dependency allows us to run tests which use JUnit 5.
  • The selenium-remote-driver dependency allows us to control the web browser that’s running in the started Docker container.
  • An SLF4J binding is required if we want to take a look at the logs which are written by TestContainers. Our example uses Log4J 2.11.2 for this purpose.

Let’s move on and find out how we can get the required dependencies with Maven.

Getting the Required Dependencies With Maven

After we have added the required dependencies to our POM file, its dependencies section looks as follows:

<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.11.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.11.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.12.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers</artifactId>
        <version>1.10.6</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>1.10.6</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>selenium</artifactId>
        <version>1.10.6</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-api</artifactId>
        <version>3.141.59</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-chrome-driver</artifactId>
        <version>3.141.59</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-remote-driver</artifactId>
        <version>3.141.59</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Next, we will find out how we can get the required dependencies with Gradle:

Getting the Required Dependencies with Gradle

We can get the required dependencies with Gradle by following these steps:

  1. Add the following dependencies to the testImplementation dependency configuration: junit-jupiter-apiassertj-core, TestContainers Core, TestContainers JUnit Jupiter, TestContainers Selenium, selenium-api, and selenium-chrome-driver.
  2. Add the following dependencies to the testRuntime dependency configuration: log4j-corelog4j-slf4j-impljunit-jupiter-engine, and selenium-remote-driver.

After we have added the required dependencies to our build.gradle file, its dependencies block looks as follows:

dependencies {
    testImplementation(
            'org.junit.jupiter:junit-jupiter-api:5.4.0',
            'org.assertj:assertj-core:3.12.0',
            'org.testcontainers:testcontainers:1.10.6',
            'org.testcontainers:junit-jupiter:1.10.6',
            'org.testcontainers:selenium:1.10.6',
            'org.seleniumhq.selenium:selenium-api:3.141.59',
            'org.seleniumhq.selenium:selenium-chrome-driver:3.141.59'
    )
    testRuntime(
            'org.apache.logging.log4j:log4j-core:2.11.2',
            'org.apache.logging.log4j:log4j-slf4j-impl:2.11.2',
            'org.junit.jupiter:junit-jupiter-engine:5.4.0',
            'org.seleniumhq.selenium:selenium-remote-driver:3.141.59'
    )
}

We can now get the required dependencies with Maven and Gradle. Next, we will learn to configure the started Docker container.

Configuring the Started Docker Container

When we configure the started Docker container, we have to:

  • Register the JUnit 5 extension (TestcontainersExtension) that integrates TestContainers with JUnit 5.
  • Start the Docker container that runs the used web browser. When we start a Docker container with TestContainers, we can use one of these two techniques:
    • We can start a Docker container before a test method is run and stop it after a test method has been run.
    • We can start a Docker container once before any test method is run and stop it after all test methods have been run.

We can configure the started Docker container by using one of these two options:

First, if we want to start the Docker container before a test method is run, we have to configure it by following these steps:

  1. Annotate our test class with the @TestContainers annotation. This annotation registers the JUnit 5 extension that integrates TestContainers with JUnit 5.
  2. Add a private and final BrowserWebDriverContainer field to our test class and annotate this field with the @Container annotation. This field contains a reference to the “Docker container” that’s managed by TestContainers.
  3. Create a new BrowserWebDriverContainer object and ensure that the started Docker container runs the Chrome web browser. Store the created BrowserWebDriverContainer object in the BROWSER_CONTAINER field.

After we have configured the started Docker container, the source code of our test class looks as follows:

import org.openqa.selenium.chrome.ChromeOptions;
import org.testcontainers.containers.BrowserWebDriverContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
 
@Testcontainers
class ontainerStartedBeforeEachTestMethodTest {
 
    @Container
    private final BrowserWebDriverContainer BROWSER_CONTAINER = new BrowserWebDriverContainer()
            .withCapabilities(new ChromeOptions());
}

Second, if we want to start the Docker container once before any test method is run, we have to configure it by following these steps:

  1. Annotate our test class with the @TestContainers annotation.
  2. Add a privatestatic, and final BrowserWebDriverContainer field to our test class and annotate this field with the @Container annotation.
  3. Create a new BrowserWebDriverContainer object and ensure that the started Docker container runs the Chrome web browser. Store the created BrowserWebDriverContainer object in the BROWSER_CONTAINER field.

After we have configured the started Docker container, the source code of our test class looks as follows:

import org.openqa.selenium.chrome.ChromeOptions;
import org.testcontainers.containers.BrowserWebDriverContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
 
@Testcontainers
class ContainerStartedOnceTest {
 
    @Container
    private static final BrowserWebDriverContainer BROWSER_CONTAINER = new BrowserWebDriverContainer()
            .withCapabilities(new ChromeOptions());
}

There are three things I want to point out:

First, most of the time we should start the Docker container only once per a test class because starting it is quite slow. In other words, if we start the Docker container before a test method is run, we write tests which are slower than they could be.

Second, we can configure the started web browser by using the withCapabilities() method of the BrowserWebDriverContainer class. When we use this method, we have to remember that:

  • The type of the Capabilities object given as a method parameter specifies the started web browser.
  • The Capabilities object given as a method parameter contains the configuration of the started web browser.

Third, the default configuration records the failed test cases to the /tmp directory. If we want to record all tests or record failed tests to a different directory, we have to to invoke the withRecordingMode() method of the BrowserWebDriverContainer class. This method has two method parameters:

  1. The used recording mode (record all tests or record only failed tests).
  2. The target directory.

Let’s move on and find out how we can write end-to-end tests which use the web browser that’s running in the started Docker container.

Writing Simple End-to-End Tests

We can write our end-to-end tests by following these steps:

First, we have to obtain a reference to the WebDriver object that allows us to control the used web browser. Also, we have to store this object in a way that helps us to remove duplicate code from our test class. We can fulfill these goals by following these steps:

  1. Add a private WebDriver field to our test class.
  2. Add a new setup method to our test class and ensure that this setup method is invoked before any test method is run.
  3. Implement the setup method by getting the required WebDriver object and store this object in the browser field.

After we have obtained the required WebDriver object, the source code of our test class looks as follows:

import org.junit.jupiter.api.BeforeAll;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.testcontainers.containers.BrowserWebDriverContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
 
@Testcontainers
class ContainerStartedOnceTest {
 
    @Container
    private static final BrowserWebDriverContainer BROWSER_CONTAINER = new BrowserWebDriverContainer()
            .withCapabilities(new ChromeOptions());
 
    private static WebDriver browser;
 
    @BeforeAll
    static void configureBrowser() {
        browser = BROWSER_CONTAINER.getWebDriver();
    }
}

Second, we have to write two simple end-to-end tests. These tests help us to ensure that our Docker container is started successfully. We can write our end-to-end tests by following these steps:

  1. Ensure that the testproject.io website has the correct title.
  2. Verify that the TestProject Blog has the correct title.

After we have written our end-to-end tests, the source code of our test class looks as follows:

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.testcontainers.containers.BrowserWebDriverContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@Testcontainers
class ContainerStartedOnceTest {
 
    @Container
    private static final BrowserWebDriverContainer BROWSER_CONTAINER = new BrowserWebDriverContainer()
            .withCapabilities(new ChromeOptions());
 
    private static WebDriver browser;
 
    @BeforeAll
    static void configureBrowser() {
        browser = BROWSER_CONTAINER.getWebDriver();
    }
 
    @Test
    @DisplayName("The testproject.io web site should have the correct title")
    void testProjectWebSiteShouldHaveCorrectTitle() {
        browser.get("https://www.testproject.io");
        assertThat(browser.getTitle())
                .isEqualTo("TestProject - Community Powered Test Automation");
    }
 
    @Test
    @DisplayName("The testproject.io blog should have the correct title")
    void testProjectBlogShouldHaveCorrectTitle() {
        browser.get("https://blog.testproject.io/");
        assertThat(browser.getTitle())
                .isEqualTo("TestProject - Test Automation Blog");
    }
}

We have written simple end-to-end tests which help us to ensure that our Docker container is started successfully. Let’s summarize what we learned from this blog post.

Summary

This blog post has taught us six things:

  • TestContainers is a library that allows us to start a Docker container before our test methods are run and stop it after our test methods have been run.
  • The @TestContainers annotation registers the JUnit 5 extension that integrates TestContainers with JUnit 5.
  • The @Container annotation identifies the field which contains a reference to the “Docker container” that’s managed by TestContainers.
  • If we want to start our Docker container before a test method is run, we have to store the started container in a non-static field.
  • If we want to start our Docker container once before any test method is run, we have to store the started container in a static field.
  • Most of the time we should start our Docker container only once per a test class because this helps us to minimize the performance hit caused by the started Docker container.

P.S. You can get the example application of this blog post from Github.

 

Let me know in the comments below if you tried out this tutorial and share your thoughts!  😎