![]() |
|---|
| Photo by CHUTTERSNAP on Unsplash |
Spring Boot Integration Testing with Testcontainers and Junit5
Spring Boot testing library offers an out-of-the box @SpringBootTest annotation that boots the entire application and gets it ready for testing.
However, getting backing services (e.g. databases, caches, message brokers) available and not interfering with development or other environments might still be a challenging task.
In this post, I'm going to introduce a way to benefit from using containers and JUnit Jupiter extension model to test a Spring Boot application in an environment that closely resembles the production environment (e.g. a real database instead of an in-memory one).
Testcontainers
Testcontainers for Java is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. a project whose purpose is to uniformize and facilitate managing containers from a jvm runtime.
When combined with the JUnit Jupiter extension model, Testcontainers for Java provide a simple yet powerful way to spin up the necessary backing services in a declarative way.
The @Testcontainers extension supports two container lifecycle modes, depending on how the contained field is
declared.
-
An instance-level
@Containerannotated container will be restarted after every test method. This is a good use-case example for a fast-bootstrap container.@Testcontainers public class CacheTests { @Container private final GenericConainer container = new GenericContainer("image-name"); } -
A class-level
@Containerannotated container will be shared across test methods heavy container. This is a better choice for heavier containers, that takes longer to become available, like a database or a message broker.@Testcontainers public class RepositoryTests { @Container private static final GenericConainer container = new GenericContainer("image-name"); }
In some cases, is worth reusing the containers across the entire set of integration tests. This can be accomplished by introducing singleton containers in a common class that every test-class can extend.
Using static initializers helps to bind the necessary dynamic parameters to the Spring configuration keys using system properties.
However, using base classes comes with some limitations, as tests that need to reuse multiple containers introduce complex hierarchies.
One way to overcome these limitations is to introduce custom extensions for each container.
public class TestcontainersExtensions {
public static class Postgres implements Extension {
private static final PostgreSQLContainer<?> container;
static {
container = new PostgreSQLContainer<>("postgres:15.2");
container.start();
System.setProperty("spring.datasource.url", container.getJdbcUrl());
System.setProperty("spring.datasource.username", container.getUsername());
System.setProperty("spring.datasource.password", container.getPassword());
}
}
public static class Redis implements Extension {
private static final GenericContainer<?> container;
static {
container = new GenericContainer<>("redis:7.0.9");
container.withExposedPorts(6379).start();
System.setProperty("spring.data.redis.host", "localhost");
System.setProperty("spring.data.redis.port", String.valueOf(container.getMappedPort(6379)));
}
}
}
This way, infrastructure details like image name/version and Spring configuration are moved away from test classes and
contained in the custom Extension implementations.
Individual test-classes can choose only the extensions they need. At the same time, extensions imported in multiple tests will reuse the containers they manage.
@SpringBootTest
@ExtendWith(TestcontainersExtensions.Postgres.class)
public class SomeRepositoryTests {
}
@SpringBootTest
@ExtendWith({TestcontainersExtensions.Postgres.class, TestcontainersExtensions.Redis.class})
public class SomeRestControllerTests {
}
