Integration test with TestContainer

Posted on Leave a comment
Container
Reading Time: 4 minutes

Test the integration layer of your java application with a real Postgres instance using the TestContainer library

Creating efficient integration tests is a common challenge when building microservices. Ensure the environment equality among the development and the production environment can be a pain. Mainly when the tools and services require too much hand configuration. When this happens, we usually take a short cut to adopt lightweight tools for the development and test environment. This makes the difference between the environments even bigger. 

The guidelines Twelve-Factor Apps tell us that environment equality is one of the success keys to build cloud-native applications. Developing and testing on similar environments allow us to create resilient applications that bring safety and speed to deploy.  

In this blog post, we will see how to create an integration test using the Testcontainer library. The test will start a PostgreSQL container and run reading and writing tests on a real database instance. 

What is Testcontainer?

Testcontainer is a java library that gives us support to create unit tests. It provides lightweight and throwaway instances of common databases or any application that ran into a Docker container. It also makes easy the creation of integration tests using container and without using complex configurations 

Pre-requirements

To run the example code available on Github, you will need to have installed the git and an IDE. Besides that, you also need to have the Docker and be able to perform the following command:

docker info

If you are using Linux, look that the command does not have the “sudo” prefix. You must be able to execute this command without this prefix because during the test running the library will use the Docker to download images and create containers.  

To run the docker command without the “sudo” prefix, your user should be added to the docker user group by the following command: 

sudo usermod -aG docker $USER

After running this command you will need to log-off and log-in to the changes take effect. 

Example project

This project has an integration test that starts a PostgreSQL container, sets up the database initial status running a SQL script, runs the test and in the end, destroys the container.  

To see the code, clone the git repository running the following command:

git clone https://github.com/educostadev/poc-testcontainers.git

Import the project in your IDE as a Maven project. If you use IntelliJ, this link has more information about how to import a Maven project. 

The Testcontainer integrates well with JUnit4 and JUnit5 using dependencies specified within the pom.xml. Open the pom.xml file from the code example and look at the dependencies responsible for the use of Testcontainer with JUnit5.

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

If you are using a database already supported by Testcontainer then you will need to add in your project the correct dependencies. Here in this link, you can find a list of databases supported by the Testcontainer. In our example, we are using a PostgreSQL instance, and the following dependency is required:

<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>postgresql</artifactId>
   <scope>test</scope>
</dependency>

The project files follow a standard Spring Boot application. Within the application.yml file, you can find all the database connection entries. Pay attention to the use of environment variables named DB_URLDB_USERNAME, and DB_PASSWORD.

server:
  port: 8083
logging:
  level:
    ROOT: info
    org.hibernate.tool.hbm2ddl: debug
    org.hibernate.SQL: debug
    org.hibernate.type.descriptor.sql: trace
spring:
  datasource:
    url: {DB_URL}
    username:{DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 5
      connection-timeout: 20000
    initialization-mode: always
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: create

The DummyDao class makes use of the jdbcTemplate library to execute SQL queries in the database. This is the class that will be tested using the PostgreSQL container. 

@Repository
public class DummyDao {
  @Autowired
  private JdbcTemplate template;
  public Dummy findDummyById(int id) {
    return template
        .queryForObject("SELECT * FROM dummy_test where id=?", new Object[]{id},
            new DummyRowMapper());
  }

  public List readAll() {
    return template.query("SELECT id,name_value FROM dummy_test", new DummyRowMapper());
  }

  public int save(Dummy dummy) {
    return template.update("INSERT INTO dummy_test (id,name_value) VALUES (?,?) ",
        new Object[]{dummy.getId(), dummy.getName()});
  }

  static class DummyRowMapper implements RowMapper {
    @Override
    public Dummy mapRow(ResultSet rs, int rowNumber) throws SQLException {
      return new Dummy(rs.getInt("id"), rs.getString("name_value"));
    }
  }
}

Look at the test bellow, it injects a DummyDAO instance and the following methods execute a read and write integration tests. Because this is an integration test, we are not mocking the behavior of the DummyDAO class, this means that a real connection with the database is required. 

A static instance of the PostgresSQL container is created in the class beginning. Even though this instance is not used in the class body, it is responsible to create and start the container. 

The static initialization of the attribute allows us to reuse the same container instance in other running tests. This avoids that a new container is created and destroyed on an every executed test. Tha @TestContainer and @Container annotation takes care of the container life cycle and allows that it be started once and destroyed when the test ends. 

@SpringBootTest
@ContextConfiguration
@Testcontainers
public class DummyDaoTest {
 
@Container
public static PostgreSQLContainer postgreSQLContainer = CustomPostgresContainer.getInstance();
 
@Autowired
DummyDao dao;
 
@Test
void injectedComponentsAreNotNull() {
  assertNotNull(dao);
}

@Test
void save_new_value() {
  Dummy value = new Dummy(1000, "dummy");
  int affectedRows = dao.save(value);
  assertEquals(1, affectedRows);
}

@Test
void read_all() {
  assertThat(dao.readAll()).isNotEmpty();
}

}

The CustomPostgresContainer class is responsible for download the image, create and start the PostgreSQL container in the alpine version. You could use any available image from Docker Hub, or even a customized image. 

In this demonstration example, the container is started and a SQL Script is loaded to set up the database with some data. 

The overridden method start() plays an important role in this test infrastructure. It gets the URL, user, and password from the started container and writes these values on environment variables. This allows the test to connect with a real database instance. 

public class CustomPostgresContainer extends PostgreSQLContainer {

  private static final Logger logger = LoggerFactory.getLogger(CustomPostgresContainer.class);
  private static final String IMAGE_VERSION = "postgres:alpine";
  private static CustomPostgresContainer container;

  private CustomPostgresContainer() {
    super(IMAGE_VERSION);
  }

  public static CustomPostgresContainer getInstance() {
    if (container == null) {
      container = new CustomPostgresContainer();
    }
    return container;
  }

  @Override
  public void start() {
    super.start();
    logger.debug("POSTGRES INFO");
    logger.debug("DB_URL: " + container.getJdbcUrl());
    logger.debug("DB_USERNAME: " + container.getUsername());
    logger.debug("DB_PASSWORD: " + container.getPassword());
    System.setProperty("DB_URL", container.getJdbcUrl());
    System.setProperty("DB_USERNAME", container.getUsername());
    System.setProperty("DB_PASSWORD", container.getPassword());
  }

  @Override
  public void stop() {
    //do nothing, JVM handles shut down
  }
}

The summary of the test execution flow using the Testcontainer is the following: 

  • The first test from the tests tacks is started;
  • The getInstance() method from CustomPostgresContainer class is invoked. 
  • The Testcontainer library checks if the required image exists locally;
  • If the image does not exist locally, it is downloaded from the docker hub;
  • A container is created, based on the downloaded image and the database is started;
  • All the following tests use the started database;
  • At the end of all test execution, the container is destroyed. 

Conclusion

In this article, we saw how the Testcontainer library makes easy the creation of test integration with the use of docker images. It allows us to decrease, the environment disparity between the development environment and production environment, it also brings safety to deploy. The library is evolving daily and brings native support to many other databases and allows you to create your image.  In this videoKevin Wittek one of the Testcontainer committers show us more about the library. 

Have you created integration tests that use real instances of services? What tools did you use? Drop a comment below! 

Leave a Reply

Your email address will not be published. Required fields are marked *