|

Testcontainers library for integration tests

Sometimes when I needed to write integration tests, I was forced to mock an external system or service from the java tests level. It wasn’t enjoyable and what is more, it required spending many hours on creating a codebase which tried to behave like a real system. It is dangerous and on the other hand, it would be nice to check if our feature works fine with a live service. What if I say that it is possible to run a docker container with the external dependency from a java test context? Does it sound cool? So with the Testcontainers library, we are able to do it without any complicated configuration or spell crafting.

The first look at Testcontainers

To start using the Testcontainers we need to ensure that we have running Docker daemon in the background and then include some dependencies into our project. For this article, I created a simple maven project that utilizes JUnit 5 with a test which needs MongoDB. Let’s define some pom.xml content that will be required.

<dependency>
	<groupId>org.junit.jupiter</groupId>
	<artifactId>junit-jupiter</artifactId>
	<version>5.8.2</version>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>org.testcontainers</groupId>
	<artifactId>testcontainers</artifactId>
	<version>1.17.3</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.testcontainers</groupId>
	<artifactId>junit-jupiter</artifactId>
	<version>1.17.3</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.testcontainers</groupId>
	<artifactId>mongodb</artifactId>
	<version>1.17.3</version>
	<scope>test</scope>
</dependency>

The Testcontainers project has many predefined modules which could be used out of the box. It gives us also the possibility to set them up in a more specific way if it is necessary. As you can notice, I included the MongoDB module but there are many many more like Databases, queues, ElasticSearch or Claud engines (you can check them here).

Now we can write our first test. Check the example below. I used the MongoDBContainer and passed the docker image name of MongoDB to its constructor. This construction will download the MongoDB docker image at the beginning of running a test class unless we already have one in our local docker images list. Then, in the test method, I manually (for now) started the container and stopped it at the end.

Unfortunately, the process of downloading an image can take some time and it depends on the image size but if this image is already available on a machine with docker, everything takes seconds. You need to take it into consideration when you would like to run your tests in CI/CD pipeline. It would be nice to do it in an environment with the already prepared required image, it will save time.

public class MongoDBContainerTest {

    private final MongoDBContainer mongo =
            new MongoDBContainer("mongo:5.0.9");

    @Test
    public void shouldStartContainer() {
        mongo.start();

        assertTrue(mongo.isRunning());

        mongo.stop();
    }

}

For me, it is so cool and so simple. Some maven dependencies and only one field of type of needed container. But fasten your seat belts and get ready. It is only 10% of the Testcontainers’ power.

Running of any docker image

With the Testcontainers, we are able to run any docker image. We do not need to limit ourselves to predefined modules. To reach this goal we have to use GenericContainer class. To show you its usage, I added the Redis client dependency (Jedis) to the pom.xml and prepared the service which allows reads and writes in the cache.

public class RedisCache {

    private final Jedis jedis;

    public RedisCache(Jedis jedis) {
        this.jedis = jedis;
    }

    public RedisCache(String host, int port) {
        this(new Jedis(host, port));
    }

    public void put(String key, String value) {
        jedis.set(key, value);
    }

    public Optional<String> get(String key) {
        return Optional.ofNullable(jedis.get(key));
    }

}

Now, I want to test my RedisCache class with the real Redis. Do you remember the manual starting and stopping the previous container? In this case, I used the @Testcontainers annotation. It is the extension of JUnit Jupiter and provides automated startup and stop of containers used in the test class with its. @Testcontainers scans class and finds all fields that are annotated with @Container and handles their lifecycle. It can be used also on a superclass as well.

The GenericContainer constructor consumes the name of the docker image and as previously, tries to download it and then run. Furthermore, we have an option to configure a logger to handle logs from inside of the container, we can configure ports or put a condition when we acknowledge our container is ready. The interface of the GenericContainer is really flexible and allows us to configure many more custom options for our container and I encourage you to read it.

@Testcontainers
public class RedisCacheTest {

    private static final Logger LOGGER = 
            LoggerFactory.getLogger(RedisCacheTest.class);

    @Container
    private final GenericContainer<?> redis =
            new GenericContainer<>(DockerImageName.parse("redis:7.0.4"))
                    .withLogConsumer(new Slf4jLogConsumer(LOGGER))
                    .withExposedPorts(6379)
                    .waitingFor(Wait.forListeningPort());

    private RedisCache sut;

    @BeforeEach
    public void beforeEach() {
        sut = new RedisCache(redis.getHost(), redis.getFirstMappedPort());
    }

    @Test
    public void shouldPutValue() {
        // given
        String key = "Batman";
        String value = "Bruce Wayne";

        // when
        sut.put(key, value);

        // then
        assertEquals(Optional.of(value), sut.get(key));
    }

    @Test
    public void shouldReturnOptionalEmpty() {
        // given
        String key = "Joker";

        // when
        Optional<String> result = sut.get(key);

        // then
        assertEquals(Optional.empty(), result);
    }

}

Documentation of Testcontainers and project site

Testcontainers project is pretty well documented by its creators. There are many examples and ways how to run different containers. Bellow, I included some pages which I think could be useful for the first step into this topic.

All examples from this post are available on my GitHub here

Testcontainers library is the perfect way for running application integration tests which require external dependencies such as databases, queues or whole services. Provides many data access layers completely out of the box. But what is important for me, it eliminates the need for writing not perfect mocks.

The same programmers as us work everywhere.
Oskar K. Bogacz

Similar Posts