DEV Community

Cover image for Stop suffering with tests, use TestContainers
Alex Rabelo Ferreira
Alex Rabelo Ferreira

Posted on • Edited on

Stop suffering with tests, use TestContainers

Developers need to work with two or more systems during the workday. The quality of these systems depends on the integration and unit tests. It requires a pipeline and a big infrastructure. The pipeline run tests in a server similar to the production server called staging. According to [1,2], the cost to maintain a server is 2k to 15k in production. It is a high cost depending on the company. The cost duplicates when needs an environment for production and another for staging. But a pipeline that runs tests in a staging environment can result in a lot of false positive errors when a dependent system is down or a team is deploying a new version. Also, a developer team can lead to other problems, the high platform amount to install, the difference between platforms' versions per developer, the difficulty to update the platform version, and the difference between platforms' versions in production and developer environments.

A rest API system needs at least one database and a message broker system. But a real market system needs a cache service, and storage system and the list of platforms can increase a lot according to market requirements. So, to run locally the system, a developer needs to install all that platforms. The number can increase exponentially when has more systems. The developer needs to install different versions of each platform to each system. Another problem is that a developer can install a platform with version x and another developer install version z. The first developer can create a test that works in the version x, but doesn't work in version z. So, the second developer can not run the tests and require help to discover the error because of the difference in the versions. A similar problem is when a bug happens in production and don't happen locally because of difference in the versions. In addition, when a new developer tries to set up the developer environment, the code cloned doesn't have the version of the platforms. So, he needs to install the lasted version available to run the tests. The end of history is that a lot of tests will fail and in the worst case he won't run the system locally without the help of a senior developer.

This article is to help developers to waste less money and to continue increasing the system delivery quality. We will show how can use two tools, Docker and TestContainers to decrease the cost to test systems with Java. Use Docker to provide the infrastructure to your dependencies and the TestContainers is a library that permits you to create different scenarios for your tests. Therefore, the reader will learn how to use these tools to create your test environment easily, having the lowest cost and working without difficulties with a developer team.

Creating infrastructure with Docker

All software needs some infrastructure like a database or a message broker. The basic process for a developer to test the software in the development stage is manually creating the environment in your workstation. Docker is a tool that can help the developer to run each dependency in containers. Find more about Docker at Docker Official.

Provisioning with TestContainers library

The library TestContainers make it easy to create all the environments for your tests at the start of the test platform. It permits you to create any type of container programmatically. You can set the ports, the networks, version of the containers and can set the correct configuration for each test. Find more information at TestContainers quick start.

Using Test Containers

First of all, you need to add the library to your dependency management. In Gradle you can do that:

testImplementation "org.junit.jupiter:junit-jupiter:5.8.1"
testImplementation "org.testcontainers:testcontainers:1.17.2"
testImplementation "org.testcontainers:junit-jupiter:1.17.2"
Enter fullscreen mode Exit fullscreen mode

There are two modes to set up containers in your tests, shared container, and local container. The first type is to create and stop the container only when JVM stops and the second is to create a new container for each method test. But in both modes, you create a container for your test starting from the creation of a class to your container or set up a library that creates a container for you. You can see below an example to create a container class for Postgres:

public class CustomPostgresContainer extends PostgreSQLContainer<CustomPostgresContainer> {

   public CustomPostgresContainer() {
      super(DockerImageName.parse("postgres:13-alpine").asCompatibleSubstituteFor("postgres"));
      withUsername("postgres");
      withPassword("<password>");
      withDatabaseName("<db-name>");
      withExposedPorts(5432);

      start();
   }
}
Enter fullscreen mode Exit fullscreen mode

After that, you can create a container for your test using this class. An observation here is the need for a new dependency that has the class PostgreSQLContainer:

testImplementation 'org.testcontainers:postgresql:1.17.6'
Enter fullscreen mode Exit fullscreen mode

Now look at an example test using that class:


public class ExampleRestIT extends IntegrationTest {

   private CustomPostgresContainer postgresContainer = new CustomPostgresContainer();

   @Test public void shouldStartingAContainer() {
      postgresContainer.start();

      Assertions.assertTrue(postgresContainer.isRunning());
   }
}
Enter fullscreen mode Exit fullscreen mode

This scenario is to have control of the container. There is an annotation that will inform to TestContainers what container needs to be started and stopped for each method test. You can see an example below:

@Testcontainers
public class ExampleRestIT extends IntegrationTest {

   @Container
   private CustomPostgresContainer postgresContainer = new CustomPostgresContainer();

   @Test public void shouldStartingAContainer() {
      // postgresContainer.start(); ------ Dont need any more

      Assertions.assertTrue(postgresContainer.isRunning());
   }
}
Enter fullscreen mode Exit fullscreen mode

Using a web platform

But this is a test without any web platform like Quarkus or Spring. A setup of TestContainers to Quarkus for example needs some new classes. But, before we need to understand our requirements. Depending on your scenario, you will need to start and stop a container for each class of tests. In another case, you can reuse one container for all tests. For the first case, create a class that implements a QuarkusTestResourceLifecycleManager. For example the class below:

public class SharedContainerResource implements QuarkusTestResourceLifecycleManager {
    protected CustomPostgresContainer POSTGRES_CONTAINER = new CustomPostgresContainer();

    @Override
    public Map<String, String> start() {
        return POSTGRES_CONTAINER.configureProperties();
    }

    @Override
    public void stop() {
        POSTGRES_CONTAINER.stop();
    }
}
Enter fullscreen mode Exit fullscreen mode

With this class, you can control the creation and destruction of a container. When the Quarkus context starts, it will create the container. After, it will call the method start to get all the properties to replace these values. For example, the port to connect to the Postgres database. When the Quarkus context stops, call the method stop and we can stop the container. If we don't need a new container for each test class, you can remove the stop function and create a static field like these:

public class SharedContainerResource implements QuarkusTestResourceLifecycleManager {
    protected static final CustomPostgresContainer POSTGRES_CONTAINER = new CustomPostgresContainer();

    @Override
    public Map<String, String> start() {
        return POSTGRES_CONTAINER.configureProperties();
    }

    @Override
    public void stop() {
        // removed the stop function of the container. The TestContainer library will destroy automatically when the JVM stops
    }
}
Enter fullscreen mode Exit fullscreen mode

With that configuration, we can create a container to the context, and at the end of execution, the TestContainer library will automatically stop the container. We recommend this feature for scenarios where you have independent tests and don't need new infrastructure, like a clean database. Using this mode you can waste less time running the tests than using a local container.

You can find more details in TestContainers quick start from the official documentation.

Environment

We used the following versions:

  • TestContainers: 1.17.2
  • Gradle: 6
  • Docker: 20
  • Quarkus: 1.13.6.Final
  • Java: 11

This example code is in this repository.

Conclusion

In this article, we show how to create integration tests with containers to simulate any kind of environment. We also show the two modes of setting up the containers in your tests and the differences between them. Also, we gave examples to use the library TestContainers with the Quarkus platform.

However, in this article, we did not cover how to use TestContainers with other web platforms like Spring. In Addition, more libraries integrate with Docker in other languages. The community created a lot of repositories in the TestContainers's GitHub account to share binds to other languages.

If you like this content and want to talk more about a problem that you are facing in your day aligned with tests, you have a special chance to book on my calendly this week. It will be a pleasure to help you solve your problem.

Top comments (0)