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:
- Know what TestContainers is.
- Can get the required dependencies with Maven and Gradle.
- Understand how we can configure our test cases.
- Can write simple end-to-end tests which use a dockerized web browser.
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:
- Docker
- Java 8 (or higher)
- A JVM testing framework. We can use either JUnit 4, JUnit 5 or Spock Framework . Also, we can use other popular testing frameworks (such as TestProject.io that can help you integrate test automation within your CI flow ), as long as we write the code that starts and stops the used Docker container.
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:
- Add the following dependencies to the
testImplementation
dependency configuration:junit-jupiter-api
,assertj-core
, TestContainers Core, TestContainers JUnit Jupiter, TestContainers Selenium,selenium-api
, andselenium-chrome-driver
. - Add the following dependencies to the
testRuntime
dependency configuration:log4j-core
,log4j-slf4j-impl
,junit-jupiter-engine
, andselenium-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:
- Annotate our test class with the
@TestContainers
annotation. This annotation registers the JUnit 5 extension that integrates TestContainers with JUnit 5. - Add a
private
andfinal
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. - Create a new
BrowserWebDriverContainer
object and ensure that the started Docker container runs the Chrome web browser. Store the createdBrowserWebDriverContainer
object in theBROWSER_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:
- Annotate our test class with the
@TestContainers
annotation. - Add a
private
,static
, andfinal
BrowserWebDriverContainer
field to our test class and annotate this field with the@Container
annotation. - Create a new
BrowserWebDriverContainer
object and ensure that the started Docker container runs the Chrome web browser. Store the createdBrowserWebDriverContainer
object in theBROWSER_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:
- The used recording mode (record all tests or record only failed tests).
- 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:
- Add a
private
WebDriver
field to our test class. - Add a new setup method to our test class and ensure that this setup method is invoked before any test method is run.
- Implement the setup method by getting the required
WebDriver
object and store this object in thebrowser
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:
- Ensure that the testproject.io website has the correct title.
- 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! 😎
Is there any example of how to integrate Docker execution with the TestProject Java SDK?
Not having luck with this example.
@Container
private final BrowserWebDriverContainer BROWSER_CONTAINER = new BrowserWebDriverContainer()
.withCapabilities(new ChromeOptions());
Is giving me the error: Cannot access org.testcontainers.containers.GenericContainer
When I download the example code it goes a bit better, but the test method testProjectBlogShouldHaveCorrectTitle is giving me the error: com.github.dockerjava.api.exception.NotFoundException: {“message”:”No such image: quay.io/testcontainers/ryuk:0.2.2″}
This is an issue I’ve seen elsewhere… sadly. If I recall it’s because some other dependency is outdated? So many inter-dependencies, or more accurate points of failure.
and the test method testProjectWebSiteShouldHaveCorrectTitle is failing with:
Expected :TestProject – Community Powered Test Automation
Actual :Free Test Automation For All | TestProject
So the second one appears to be working (site probably changed), so I can at least use that as a basis and try and figure what could be going wrong in my original code and why it doesn’t want to work… still TestContainer and Selenium has been nothing but problem after problem, in the time I’ve wasted trying to sort out the myriad of issues I could probably just have setup a instance without using docker…
Sadly i work in a team with people who really like complexity, so the more frameworks and tools the better. ThusI’ll sadly have to continue trying to figure this out…