TestContainers — Empowering Integration Tests

Preeti Saini
TestVagrant
Published in
7 min readJun 5, 2020
source: https://www.testcontainers.org/

Software organisations today rely heavily on continuous integration and continuous delivery pipelines for building, testing and deploying applications. A smooth build promotion strategy involves an automation testing strategy at each phase with real-like environment.

One of the most popular approach is to setup test environment on CI pipeline (or on any server or even locally), is to run the docker containers for services/applications required for running the tests beforehand. But with this approach comes the caveat:

  1. We often use shared environment for integration tests across teams, as using a separate instances for these dependencies for each test or teams is a costly affair.
  2. This shared status leads database integration tests or any interface integration test, needing a clean state or desired/known state, to flaky, non reliable, forcing teams to re-think on inclusion of these test as part of integration testing.
  3. Also any data mutation by one team effecting other teams execution, lead to stringent processes for Test data management. This in-turn adds additional overheads to testing and further leads to exclusion of some tests from integration suite again.
  4. All integration tests aren’t fast enough, but running these tests concurrently, leads to resource conflict issues. This means that the tests should be isolated and as such need to avoid resource conflicts.

Well, we have now a java library known as TestContainers which does compliments the integration testing well, catering to most of the concerns mentioned above.

TestContainers

As per the official documentation

Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

TestContainers Library allows us to use docker containers with in our tests and hence provides the external dependencies like database layer , stream processing(kafka, pulsar), selenium, AWS mocks(localStack), any docker image or any application as defined in docker compose file — contained within our tests. It will spin up the required Docker containers for the duration of the tests and tear down once the test execution has finished.

Key benefits:

  1. Self-contained integration tests. Build and run integration tests locally, or on CI or any machine. All we need is to define our dependency as a java object with in our tests. For example:

2. Faster feedback loop for integration tests. As seen above, now my external dependencies are embedded in my tests, can run integration tests in my local, enabling faster tests results availability and hence fastening the feedback loop.

3. Provides isolated instances of containers, hence caters the need of parallel execution. We can define container with “@Rule”, which will start the container before each test and will stop & destroy the container as soon as the test completes. And if defined as “@ClassRule”, container will be started before any test in the class and will stop & destroy the container only after all the tests in the class are ran.

4. Starts the containers with clean or a known state. Hence the test results are always reliable as it eliminates the data contamination/corruption reason for test failures.

5. Port randomisation. Starts the containers on random available free ports. Need not to worry about the port conflicts anymore while deploying on CI or on any machine. For example, the below will start the redis container on any free available port and binds it to 6379 .

6. Only additional installation required -> Docker.

And many more..

Test Dependencies Supported:

Implementation

Prerequisites:

  1. Docker
  2. JVM supported testing framework -> Junit4, Jupiter/Junit5, Spock
  3. TestContainer dependency in build class path. Example:
For TestContainer Dependency

Let’s see some of the very common use cases of the TestContainers

Use Case1 — Testing integration with a Caching System:

We have a class that depends on the cache interface which does basic CRUD operations. We decide to use a redis implementation for the cache interface. With TestContainers, GenericContainer support, can provide redis cache instance to my tests without having redis installed in the host.

Add the below dependencies in the build file. Here I am using gradle as a build tool.

Initialise the redis container using the GenericContainer type and make tests to communicate to the redis container using any redis client library (have used lettuce library in my case). For simplicity, have provided the redis cache implementation in the setUp method and the test method deals with simple put and read commands of redis.

GenericContainer provides the most simplest and most popular interaction to start the container for our tests. We can specify any image (public or private) and TestContainer will start the docker container for it, making it easy to use virtually any container images as temporary test dependencies.

Use Case 2: Testing Database Integration layer

Need a MySQL database server for data-access-layer integration tests. With TestContainer, we can spin up the MySQL database container with clean or known state, for the duration of the tests and tear down once the test execution has finished.

Add the below dependencies in the build file. Here I am using gradle as a build tool.

Now initialise our MySQL container. The easiest way to do so is:

TestContainer will start the container with MySQl database server with clean state, with default username and password, on a random available free port on the host. If needed we can customise the username , password and database name by:

We can now use the TestContainer’s methods getJdbcUrl(), getUsername(), getPassword() for setting up database connection. For simplicity, the below example sets the database connection as part the test and then perform basic CRUD operations.

TestContainer library has specialised support for many, almost every database.

It is also possible to run the given database as a generic container, but the easiest way is to use specialised container.

Use Case 3: Testing UI with Selenium WebDriver

We have been asked to run our UI tests on CI pipeline but the remote selenium grid setup is not yet ready . With TestContainer’s BrowserWedDriverContainer, we can containerised our UI tests on Chrome or Firefox browsers (only supported browsers by TestContainer’s as of today)and can run our tests on CI or any server without any need of remote selenium grid setup. To do so, all we need is to add the below dependencies in our project.

TestContainer looks at the selenium version we have in our classpath and pulls the compatible selenium image from the hub.

Initialise the WebDriver container with Chrome or Firefox browser in our test class as :

The below example, opens up google search page on the container’s chrome browser, search for keyword “TestContainers” and asserts if the search page is loaded or not — all in one in the test.

As the TestContainer runs selenium tests on headless mode, it also gives us the option of either recording all the tests or just the failed tests. For example we can set the VncRecording to record all tests and save the recoding at ./target/ directory as below:

There are many more use cases of TestContainer’s which are also quite common. For more details, visit official site of TestContainers.

Conclusion

TestContainers provides a good option of containerising external dependencies with in our tests, thus providing a quicker turnaround time for integration tests and hence fastening the feedback loop for java applications, bringing a shift left approach. TestContainers does compliments the scrum teams involved in developing, deploying and managing the micro-services (using java) in parallel, very well. Teams here can now run most of the integration tests against the required microservice under test, independently and at a very early stage in development cycle — along side unit testing, and also in CI pipeline. “Works on My Machine” or can “Can only test in integration environment” is no longer an excuse for the integration tests. With TestContainers, we no longer need to manage the external dependencies ourselves, while running test locally or in CI, which is a huge win.

--

--