Skip to content

Testcontainers

Siva. J edited this page Mar 16, 2023 · 1 revision

Integration Tests with Testcontainers

TestContainers is a library that helps you run module-specific Docker containers to simplify Integration Testing.

These Docker containers are lightweight, and once the tests are finished the containers get destroyed.

How does it work?

  • Test containers is a Java library that provides functionality to handle a docker container. We can start any container by using the GenericContainer with any docker image, one of the specialized containers (e.g PostgreSqlContainer) provided by a module, or by programmatically creating our own image on the fly.
  • To bind the container to the lifecycle of our JUnit tests, we can use the provided integration. Test containers also ensure that the application inside the container is started and ready to use, by providing a WaitStrategy.
  • TestContainers downloads the MySQL, Postgres, Kafka, and Redis images and runs in a container. The MySQL container will run a MySQL Database in a container and the test cases can connect to it on the local machine. Once the execution is over the Database will be gone – it just deletes from the machine. In the test cases, we can start as many container images as we want.
  • TestContainers supports JUnit 4, JUnit 5, and Spock

Benefits of Testcontainers

  • Integration tests will point to the same version of the database as it’s in production. So we can tie our TestContainer Database Image to the same version running on production.
  • Integration tests are a lot more reliable because both applications and tests are using the same database type and version and there won't be any compatibility issues in test cases.

Project Setup

We are setting up the template project by using Java 17, Spring Data JPA, PostgreSQL, and Gradle/Maven. and we are using JUnit 5, Testcontainers, and RestAssured for testing.

Test Dependencies

Following are the Test containers and RestAssured dependencies:

build.gradle

ext {
    set('testcontainersVersion', "1.17.6")
}

dependencies {
    ...
    ...
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:postgresql'
    testImplementation 'io.rest-assured:rest-assured'
}

dependencyManagement {
    imports {
        mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
    }
}

How to use Testcontainers?

Testcontainers library can be used to spin up desired services as docker containers and run tests against those services. We can use our testing library lifecycle hooks to start/stop containers using Testcontainers API.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerTests {
    @LocalServerPort
    private Integer port;

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14-alpine");

    @BeforeAll
    static void beforeAll() {
        postgres.start();
    }

    @AfterAll
    static void afterAll() {
        postgres.stop();
    }

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    TodoRepository todoRepository;

    @BeforeEach
    void setUp() {
        todoRepository.deleteAll();
        RestAssured.baseURI = "http://localhost:" + port;
    }

    @Test
    void shouldGetAllTodos() {
        List<Todo> todos = List.of(
                new Todo(null, "Todo Item 1", false, 1),
                new Todo(null, "Todo Item 2", false, 2)
        );
        todoRepository.saveAll(todos);

        given()
                .contentType(ContentType.JSON)
                .when()
                .get("/todos")
                .then()
                .statusCode(200)
                .body(".", hasSize(2));
    }
}

Here we have defined a PostgreSQLContainer instance, started the container before executing tests and stopped it after executing all the tests using JUnit 5 test lifecycle hook methods.

The Postgresql container port (5432) will be mapped to a random available port on the host. This helps to avoid port conflicts and allows running tests in parallel. Then we are using SpringBoot's dynamic property registration support to add/override the datasource properties obtained from the Postgres container.

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);
}

In shouldGetAllTodos() test we are saving two Todo entities into the database using TodoRepository and testing GET /todos API endpoint to fetch todos using RestAssured.

We can run the tests directly from IDE or using the command ./gradlew test from the terminal.

Using Testcontainers JUnit 5 Extension

Instead of implementing JUnit 5 lifecycle callback methods to start and stop the Postgres container, we can use Testcontainers JUnit 5 Extension annotations to manage the container lifecycle as follows:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class TodoControllerTests {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
}

The Testcontainers JUnit 5 Extension will take care of starting the container before tests and stopping it after tests. If the container is a static field then it will be started once before all the tests and stopped after all the tests. If it is a non-static field then the container will be started before each test and stopped after each test.

Even if we don't stop the containers explicitly, Testcontainers will take care of removing the containers, using ryuk container behind the scenes, once all the tests are done. But it is recommended to clean up the containers as soon as possible.

Using Testcontainers JDBC URL

Testcontainers provides special jdbc url support which automatically spins up the configured database as a container.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:tc:postgresql:14-alpine:///todos"
})
class ApplicationTests {

    @Test
    void contextLoads() {
    }
}

By setting the datasource url to jdbc:tc:postgresql:14-alpine:///todos (notice the special :tc prefix), Testcontainers automatically spin up the Postgres database using postgresql:14-alpine docker image.

For more information on Testcontainers JDBC Support refer https://www.testcontainers.org/modules/databases/jdbc/

Verify the test cases by using

$ ./gradlew test

Results

Screenshot 2023-03-08 at 4 32 51 PM Screenshot 2023-03-08 at 4 38 27 PM
Testcontainers enable using the real dependencies services like SQL databases, NoSQL datastores, and message brokers or any containerized services for that matter. This approach allows us to create reliable test suites improving confidence in our code.
Clone this wiki locally