Testcontainers

Natalia
GumGum Tech Blog
Published in
8 min readJun 22, 2021

Introduction

In this post we will discuss Testcontainers, a Java library for building more robust testing. When talking about testing, we are familiar with unit testing and integration testing. While the first aims to guarantee the testing of a single piece of code, the second one aims to test the functionality for a set of components integrated. In this second scenario is where the challenges appear more often. For GumGum’s main Advertising API, we have always done unit testing using mocks for service layers and controllers, however we wanted to expand our integration coverage. This includes things like testing the DAOs (data access layer, whichever the ORM or DB in use is) and the service layer. How do we test the database access and emulate something which is closer to the real world? How do we test a messaging queue? All these are problems and questions that many developers face when it comes to integration testing.

After exploring among several options, we came across Testcontainers and thought we would try them for our team. The reason behind this is that it would allow us to test not only the DAOs, but also the service layer in a more complete way, by validating the business logic in a more real scenario. In this post, I will go over how to set up and use Testcontainers, and how we started using them on Advertising API at GumGum.

The main idea behind Testcontainers is to provide an environment for integration testing that deals with all of the infrastructure our tests need, and make that transparent for the developers. This implies that you create the test and let the container provide a managed environment for your database, or any other service that your app needs. Since it is a Java library, it is very easy to integrate it with JUnit.

Let’s go through a very simple example to demonstrate it, a test that just runs over MySQL and does a simple query.

A simple example

The first step would be to add the correct dependencies, in this case https://www.testcontainers.org/modules/databases/mysql/:

testCompile "org.testcontainers:testcontainers:1.15.3"testCompile "org.testcontainers:mysql:1.15.3"

After that, we can write a simple test:

@Testpublic void testSimple() throws SQLException {
try (MySQLContainer<?> mysql = new MySQLContainer<>("mysql:5.5")
.withLogConsumer(new Slf4jLogConsumer(logger))) {
mysql.start();
ResultSet resultSet = performQuery(mysql, "SELECT 1"); int resultSetInt = resultSet.getInt(1); Assert.assertEquals("A basic SELECT query succeeds", 1, resultSetInt); }
}

You can check out this example and others from the GitHub repository on Testcontainers.

If we run this test, we will see in the console that a Docker container starts and creates an image for MySQL version 5.5, initializes the database, and makes it available for use. The test should be successful.

The purpose of this example is to showcase how simple it is to start using TestContainers and configure it in an existing project. The idea of using containers is that it provides a lot of built-in images to test integrations, from databases to messaging, Kafka, Elasticsearch, and more.

Prerequisites

There are not many prerequisites, and that is an advantage too, but of course you need Docker installed on your machine. For more details, see this section of their documentation.

I recommend that you to follow this example to install JUnit5 and add it to your Java project if you do not have it already since JUnit5 has built in support for TestContainers. To do so, you can follow this short and easy tutorial. A note on this step is that if your project makes use of JUnit 3 or 4, you can still configure both it to work with 5.

TestContainers with GumGum’s Advertising API

We have integration testing that uses the embedded database H2 for the data access layer. This has some disadvantages since doing queries on H2 is not exactly the same as MySQL, and therefore is far from the real scenario we wanted to test.

For configuring Testcontainers, we have created a class that handles everything related to Testcontainers’ configuration:

public class MySQLContainerConfig extends     MySQLContainer<MySQLContainerConfig> {
...
private static MySQLContainerConfig container;
private static String DBNAME="test_containers";
private static String USER="test_containers";
private static String PWD="test_containers";
private static String MYSQL_IMAGE = "mysql:5.5";
private MySQLContainerConfig() {
super(MYSQL_IMAGE);
}
public static MySQLContainerConfig getInstance() {
if (container == null) {
container = new MySQLContainerConfig().withDatabaseName(DBNAME)
.withUsername(USER)
.withPassword(PWD)
.withLogConsumer(new Slf4jLogConsumer(logger));
}
return container;
}
@Override public void start() {
super.start();
System.setProperty("mysql.jdbc.url", container.getJdbcUrl());
System.setProperty("mysql.username", container.getUsername());
System.setProperty("mysql.password", container.getPassword());
}
...
}

We need to define a properties file that has the following entries:

mysql.jdbc.url=${DB_URL}
mysql.username=${DB_USERNAME}
mysql.password=${DB_PASSWORD}

Then a test file for a repository would look like this:

@Testcontainers@SpringBootTest(classes={MySQLConfig.class, JpaConfigTest.class})@ComponentScan(basePackages = {"com.gumgum.advertising.api.domain.mysql"})@Tag("integration")public class UserServiceIntegrationTest  {
@Container
public static MySQLContainerConfig containerConfig = MySQLContainerConfig.getInstance();

@Autowired
private DataSource dataSource;
@Autowired
private UsersRepository usersRepository;
private static String usersTable = "CREATE TABLE IF NOT EXISTS users (" +
" id INT NOT NULL AUTO_INCREMENT, " +
" email VARCHAR(255) NOT NULL, " +
" first_name VARCHAR(255), " +
" last_name VARCHAR(255), " +
...
" PRIMARY KEY (id), " +
" CONSTRAINT ix_users_email UNIQUE (email) )";
@Test
void injectedComponentsAreNotNull() {
Assert.assertThat(dataSource, Matchers.notNullValue());
Assert.assertThat(usersRepository, Matchers.notNullValue());
}
@Test
public void testUserRepository() throws SQLException {
Statement statement = dataSource.getConnection().createStatement();
statement.execute(usersTable);
Assert.assertEquals(IterableUtils.toList(usersRepository.findAll()).size(), 0);
usersRepository.save(mockUser()); Assert.assertEquals(IterableUtils.toList(usersRepository.findAll()).size(), 1);
}
...
}

@TestContainersis one of the annotations that we have available since we are using JUnit5 (it’s an extension from JUnit5 called Jupiter that has the benefit of allowing the automatic startup and stopping of containers used in a test case (in this example, we have this in another class that can be used from any of the test cases: MySQLContainerConfig).

@Containeris also useful since it’s used in conjunction with the @TestContainers annotation and allows us to indicate containers that should be managed by the Testcontainers extension.

This is a simple example of how to integrate test containers into an existing project, and hopefully it helps to showcase the simplicity of it. For this example, there are omitted classes not shown here that are the regular Spring configuration for test classes, referenced from @SpringBootTest(classes={MySQLConfig.class, JpaConfigTest.class}).

Between those classes, we need to tell Spring where the config for this tests lives:

@Configuration@EnableJpaRepositories(basePackages = {"com.gumgum.advertising.api.domain.mysql", "com.gumgum.advertising.api.commons"})@EnableTransactionManagement@PropertySource(value = "classpath:testContainersAA.properties")

This should be pretty much the same as the config we already have in place for JPA in the project.

Ideally we could use a script or a set of scripts to initialize the container, and therefore not create the tables in each test. We want to instead define the structure we want to test and execute that when starting the container, and for this there is a method withInitScript from MySQLContainer.

Since for most of the cases in Advertising API the repositories that we deal with do not present complex things to test, let’s explore an example of what a service component test would look like. Previously we would create the service using mocks for all its dependencies:

@Testpublic void testUserService() throws SQLException {
Statement statement = dataSource.getConnection().createStatement();
statement.execute(usersTable);
User createdUser = userService.createUser(mockUserRequestCreate());
Assert.assertEquals(createdUser.getEmail(), "new@gumgum.com");
User updatedUser = userService.updateUser(createdUser.getId(), mockUserRequestUpdate());
Assert.assertEquals(updatedUser.getEmail(), "test@gumgum.com");
User findByUser = userService.findUserByEmail("test@gumgum.com");
Assert.assertEquals(findByUser.getEmail(), "test@gumgum.com");
}

Now we can test in a single and very simple method all of the operations from a service. Even though this example is relatively simple, we could have a test that aims to create an initial database setup to test a set of use cases where we test all of the involved operations on those use cases.

Why adopt test containers for unit tests?

There are some advantages of using containers in the unit tests which i think are important to mention:

  • It’s easy to use and configure.
  • It gives the flexibility to add real scenarios over mocking everything or emulating it using a test embedded database.
  • Even though we haven’t mentioned it in this post, it can be applied to test service components which have more business logic. Therefore you can test the service as a single component which can be really useful in cases where we have a complex logic.
  • Allows us to test not only components that require a database connection, but also messaging systems or other kinds of data sources.

There are some disadvantages though, which is also worth mentioning:

  • There could be a configuration file that initially creates the database from a script. These scripts have to be kept up to date.
  • Running tests like this can be more time consuming, although we should keep in mind that the volume of tests that require an integration test should be less than the amount of unit tests we have.

For the time consuming disadvantage, the way I think it can be used and be more useful for developers is to create these tests and exclude them from the regular build/deployment process. This way we don’t make the build/deployment process such a nightmare…

We can add a Gradle task to just run these tests and exclude them from the regular test task. This way each developer can use it when it needs and on demand by executing a simple Gradle task.

Bonus track, excluding and including tests

JUnit 5 has the @Tag annotation that allows to tag tests. These tags can then be included or excluded from the test task in Gradle.

For example, if we tag the User test with @Tag('integration') as shown:

@Testcontainers@SpringBootTest(classes={MySQLConfig.class, JpaConfigTest.class})@ComponentScan(basePackages = {"com.gumgum.advertising.api.domain.mysql"})@Tag("integration")public class UserServiceIntegrationTest  {

Then we can do something like the following in the build.gradle file:

test {
useJUnitPlatform {
excludeTags 'integration'
}
testLogging {
showStandardStreams = true
events("skipped", "failed", "passed")
}
}
task runIntegrationTests(type: Test) {
useJUnitPlatform {
includeTags 'integration'
}
testLogging {
showStandardStreams = true
events("skipped", "failed", "passed")
}
}

Summary

Testcontainers seems like a powerful tool to build useful integration tests. There are a lot of other services that can be tested using this library apart from the examples outlined here, which goes from many databases to several AWS services, and so on (any service that has a Docker image available to use in the containers can be tested).

I think that one of the main advantages is the ease of configuration and usage. That in combination with JUnit5 makes it a very good option to improve tests for our API.

There are complex use cases that might require more extensive testing due to the complexity or the extensive database queries applied where using this may help reduce the errors and bugs when releasing to production. In Advertising API we have several places where we do a lot of queries and bulk operations that may require more extensive testing than other components.

However, we need to consider that including this as part of the build process or as part of a continuous integration process can lead to speed issues, and that’s why I suggest having this as a separate task that each developer can run/use on demand.

I hope this post has been helpful!

Thanks,

Natalia

References

https://www.testcontainers.org

https://www.baeldung.com/docker-test-containers

https://www.wwt.com/article/using-testcontainers-for-unit-tests-with-spring-and-kotlin

https://howtodoinjava.com/junit5/junit-5-gradle-dependency-build-gradle-example

https://www.baeldung.com/junit-5-gradle

https://www.baeldung.com/spring-dynamicpropertysource

https://www.baeldung.com/spring-boot-testcontainers-integration-test

We’re always looking for new talent! View jobs.

Follow us: Facebook | Twitter | | Linkedin | Instagram

--

--