Integration Testing with Docker-compose and Testcontainers
In this blog post I will explain how to perform application integration testing with Docker-Compose and Testcontainers in Spring Boot application.
In one of my previous blog post I have covered, how to do integration testing with docker-compose file by starting containers with the help of maven plugins. If you are looking to migrate docker-compose based integration tests to Testcontainers, using dokcer-compose module could be the easy starting point.
In this post, I will explain how to start containers from docker compose file with Testcontainers library and run integration tests with these containers.
We will create a simple docker compose file that will consist of postgreSQL container
# A Docker Compose must always start with the version tag.
# We use '3.3' because it's the last version.
version: '3.3'
# You should know that Docker Compose works with services.
# 1 service = 1 container.
# For example, a service, a server, a client, a database...
# We use the keyword 'services' to start to create services.
services:
# The name of our service is "postgres"
# but you can use the name of your choice.
# Note: This may change the commands you are going to use a little bit.
postgres:
# Official Postgres image from DockerHub
image: 'postgres:13.2'
# By default, a Postgres database is running on the 5432 port.
# If we want to access the database from our computer (outside the container),
# we must share the port with our computer's port.
# The syntax is [port we want on our machine]:[port we want to retrieve in the container]
# Note: You are free to change your computer's port,
# but take into consideration that it will change the way
# you are connecting to your database.
ports:
- 5437:5432
networks:
- app_net
environment:
POSTGRES_USER: postgres # The PostgreSQL user (useful to connect to the database)
POSTGRES_PASSWORD: postgres # The PostgreSQL password (useful to connect to the database)
POSTGRES_DB: eis # The PostgreSQL default database (automatically created at first launch)
networks:
app_net:
driver: bridge
Code language: YAML (yaml)
Starting containers with Testcontainers
Testcontainer has a dedicated Dockerc-Compose
module which can be used to start containers from the compose file.
This is intended to be useful on projects where Docker Compose is already used in dev or other environments to define services on which an application may rely.
Behind the scenes, Testcontainers actually launches a temporary Docker Compose client – in a container, of course, so it’s not necessary to have it installed on all developer/test machines.
We can start the containers from docker compose file with below syntax.
public static DockerComposeContainer environment = new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) .withExposedService("SERVICE-2", SERVICE2_PORT) .withExposedService("service-1", SERVICE1_PORT);
Accessing a containers in Tests
The rule provides methods for discovering how your tests can interact with the containers:
getServiceHost(serviceName, servicePort)
returns the IP address where the container is listeninggetServicePort(serviceName, servicePort)
returns the Docker mapped port for a port that has been exposed
You can start the containers from docker-compose with Testcontainers in 3 ways
Using @Container
For using Testcontainers , you need to annotate test class with @Testcontainers annotation.
In our application we are annotating base class.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
public class BaseIT {
@Autowired
protected TestRestTemplate testRestTemplate ;
protected static int POSTGRES_PORT = 5432;
}
Code language: Java (java)
Then we define container with @Container
annotation.
@Container
annotation tells JUnit to notify this field about various events in the test lifecycle.
When you define containers with @Container
annotation Junit will start and stop the container automatically based on lifecycle events.
In this method we have to declare container in each test class. This method will run separate container for each test class.
Since this method starts and stops container for each test class running tests will take more time with this method
public class DepartmentControllerIT extends BaseIT {
@Container
private static final DockerComposeContainer environment =
new DockerComposeContainer(new File("src/test/resources/docker-compose.yaml"))
.withExposedService("postgres", POSTGRES_PORT, Wait.forListeningPort())
.withLocalCompose(true);
@Test
@Sql({ "/import.sql" })
public void testGetDepartmentById() {
ResponseEntity<Department> response = testRestTemplate.getForEntity( "/department/{id}",Department.class,100);
Department dept = response.getBody();
assertEquals(100,dept.getId());
assertEquals("HR", dept.getName());
}
@DynamicPropertySource
public static void properties(DynamicPropertyRegistry registry) {
String postgresUrl = environment.getServiceHost("postgres", POSTGRES_PORT)
+ ":" +
environment.getServicePort("postgres", POSTGRES_PORT);
registry.add("spring.datasource.url", () -> "jdbc:postgresql://"+postgresUrl+"/eis");
registry.add("spring.datasource.username", () ->"postgres");
registry.add("spring.datasource.password", () ->"postgres");
}
}
Code language: Java (java)
Sample source code for this blog post can be downloaded from GitHub
[icon name=”clipboard” prefix=”fas”] Note
You might think that why not move container instantiation and dynamic property setting code to base class. I tried that option but if you have more than on test class first test class is working properly but remaining test classes are failing. There seems to be a bug in ‘DockerComposeContainer` module.
Manually starting services
In this method we start the container inside static block manually and Testcontainers takes care of stopping the container.
This method creates a single instance for the container for all the test cases. This is the preferred method for running large no. of test cases.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
public class BaseIT {
@Autowired
protected TestRestTemplate testRestTemplate ;
private static int POSTGRES_PORT = 5432;
private static Map<String,String> postgresEnvMap = new HashMap<>();
static final DockerComposeContainer environment;
static {
environment =
new DockerComposeContainer(new File("src/test/resources/docker-compose.yaml"))
.withExposedService("postgres", POSTGRES_PORT, Wait.forListeningPort())
.withLocalCompose(true)
;
environment.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> stopContainer()));
}
private static void stopContainer() {
environment.stop();
}
@DynamicPropertySource
public static void properties(DynamicPropertyRegistry registry) {
String postgresUrl = environment.getServiceHost("postgres", POSTGRES_PORT)
+ ":" +
environment.getServicePort("postgres", POSTGRES_PORT);
registry.add("spring.datasource.url", () -> "jdbc:postgresql://"+postgresUrl+"/eis");
registry.add("spring.datasource.username", () ->"postgres");
registry.add("spring.datasource.password", () ->"postgres");
}
}
Code language: Java (java)
Sample source code for this blog post can be downloaded from GitHub
Using Configuration file
You can also initialize the datasource in configuration file and and start the container in it.
This method also creates a single instance of the container for all the test cases. This is the preferred method if you are migrating from existing docker-compose integration tests to Testcontainers based integration tests.
@Configuration
public class PostgresContainerConfiguration {
private static int POSTGRES_PORT = 5432;
@Value("${spring.datasource.username}")
private String datasourceUsername;
@Value("${spring.datasource.password}")
private String datasourcePassword;
static final DockerComposeContainer environment = new DockerComposeContainer(new File("src/test/resources/docker-compose.yaml"))
.withExposedService("postgres",POSTGRES_PORT, Wait.forListeningPort())
.withLocalCompose(true)
;
@PostConstruct
public void start() {
environment.start();
}
@PreDestroy
public void stop() {
environment.stop();
}
@Bean
public DataSource getDataSource()
{
String postgresUrl = environment.getServiceHost("postgres", POSTGRES_PORT)
+ ":" +
environment.getServicePort("postgres", POSTGRES_PORT);
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("org.postgresql.Driver");
dataSourceBuilder.url("jdbc:postgresql://"+postgresUrl+"/eis");
dataSourceBuilder.username(datasourceUsername);
dataSourceBuilder.password(datasourcePassword);
return dataSourceBuilder.build();
}
}
Code language: Java (java)
Sample source code for this method can be downloaded from GithHub
You might be also interested in
References