Testcontainers is essential for creating ephemeral environments for testing your applications. However, nothing in the API is specific to those conditions, and you can use it to programmatically manage any environment.
One of the more common scenarios is creating an environment for your application when you run it locally. Some application frameworks integrate with Testcontainers to provide this functionality out of the box:
- Quarkus has Dev Services.
- Micronaut has Test Resources.
- Spring Boot can be configured too.
In this chapter we'll configure our Spring Boot application to use Testcontainers at Development Time.
Verify your applications has the org.springframework.boot:spring-boot-testcontainers
dependency.
This application requires configuring Postgres, Kafka, and Redis instances to run locally. Our tests already have all the code to instantiate these services, configure them, and the application to use them. However, for easier time running the application locally, we should refactor this code to be a part of the application initialization lifecycle.
We'll use a TestConfiguration to encapsulate the environment creation.
Create a class ContainerConfig
in the Test classpath that will implement the TestConfiguration
:
@TestConfiguration(proxyBeanMethods = false)
public class ContainerConfig {
@Bean
@ServiceConnection(name = "redis")
public GenericContainer redis() {
return new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
}
}
The example above contains a @Bean
definition for a Redis container.
Create similar @Bean
definitions for PostgresContainer
and KafkaContainer
; you can copy the container configuration from the test classes.
Spring Boot 3.1 has integration with Testcontainers so containers exposed as Bean
will be started by the framework automatically.
The ServiceConnection
helps Spring Boot to configure itself to use the services (similar to how the @DynamicPropertySource
method before).
Now we'll use this configuration class to create the environment for our tests and running application locally.
Remove the container configration from the AbstractIntegrationTest
class, and remove the @DynamicPropertySource
method.
Instruct the test to use the configuration where containers are initialized as beans, by adding the classes
property to the SpringBootTest
:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
}, classes = {ContainerConfig.class})
Now you can check whether the tests still pass normally.
The ContainerConfig
class is on the test classpath and unavailable to the application classpath.
This is by design because we don't want to make Testcontainers and other testing libraries available in the application classpath.
So to use the ContainerConfig
, we need to create a separate entry point for running the application.
Create a TestDemoApplication
class, that uses the actual DemoApplication
and also uses the ContainerConfig
we implemented above:
public class TestApplication {
public static void main(String[] args) {
SpringApplication
.from(DemoApplication::main)
.with(ContainerConfig.class)
.run(args);
}
}
Running this class will run the DemoApplication
, and you should see from the logs that the application is started successfully.
Stopping the application will remove the containers just like Testcontainers cleans up the environment after running the tests.
You can detach the lifecycle of the containers from the lifecycle of the Spring context by using Dev tools.
Add the spring-boot-devtools
dependency.
Annotate beans and bean methods with @RestartScope
.
When reloading the project changes with devtools you can see that the containers are not restarted.
Once the application is running, you might want to connect to the database to inspect the data in the database. By default, Testcontainers starts the containers and map it to a random available port on the host. So, you need to find out the mapped port to connect to the database.
Instead, we can use Testcontainers Desktop fixed ports support to connect to the database.
Open Testcontainers Desktop, and select the Services
-> Open config location
.
It will open a directory with the example configuration files for commonly used services.
Copy the postgres.toml.example
to postgres.toml
, and update it's content to the following:
ports = [
{local-port = 5432, container-port = 5432},
]
selector.image-names = ["postgres"]
This configuration will map Postgres container port 5432 to the host port 5432. Now, when you run the application, you can connect to the database using the following connection details:
host: localhost
port: 5432
username: test
password: test
database: test
During the development of the application, you might want to stop and start the application multiple times.
Instead of creating the containers from scratch every time, you can use the reuse
feature of Testcontainers.
- Enable the
reuse
feature in Testcontainers Desktop by enabling Preferences -> Enable reusable containers. - Update the Containers configuration to use the
reuse
feature with.withReuse(true)
:
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgres() {
return new PostgreSQLContainer<>("postgres:16-alpine").withReuse(true);
}
Now, when you restart the application, you can see that the container is reused from the previous run.
BY enabling the reuse
feature, Testcontainers won't remove those reusable containers automatically
when the application is stopped or test execution is done.
If you no longer need the container, you can remove it from the Testcontainers Desktop -> Terminate Containers.
When you run Testcontainers tests, the containers are automatically stopped and removed after the test execution is done. This is a great feature to keep the environment clean and prevent resource leaks. But, sometimes you might want to debug the test and inspect the data in the database or the messages in the Kafka topic.
You can use the Testcontainers Desktop Freeze containers shutdown feature that will prevent the container shutdown allowing you to debug the issue.