In this post, we will take a look at how we can use Testcontainers for our integration tests. Testcontainers will allow us to write integration tests making use of containerized databases, message queues, web browsers, etc. without a dependency on a local installation.
1. Introduction
A common problem when writing integration tests, is the dependency on installed components where the integration tests are supposed to run. Think about databases, message queues, web browsers, etc. Wouldn’t it be great when we could spin off a database instance in a Docker container when starting our integration test? We would always start with a clean database and our integration tests could run on any machine. This is where Testcontainers are designed for. Testcontainers is a Java library which can be used in JUnit tests.
In the next sections, we will create an integration test which is dependent on a PostgreSQL database. We will use the small CRUD Spring Boot application from our previous post. In short, the application is able to add an employee and to retrieve a list of all employees by means of a REST interface. The data is being stored in a PostgreSQL database.
The source code in this post is available at GitHub in branch feature/testcontainers
.
2. Create Integration Test
First thing to do is to create an integration test. We will do so for retrieving the list of employees. In a real application it is advised to separate your unit tests from your integration tests. We are not going to explore this in depth in this post, but we will use Spring profiles in order to be able to run our tests when a specific profile is used. If you want to separate unit tests from integration tests, you can simply create and use different profiles based on this example.
In our integration test, we call the getAllEmployees
URL, check the HTTP response status and check the content which is retrieved. Remember that we used Liquibase for creating and migrating the database and that we inserted one employee in the database when the Spring profile test
is being used.
@SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc class MyliquibasePlanetApplicationTests { @Autowired private MockMvc mockMvc; @Test void contextLoads() { } @Test public void getAllEmployees() throws Exception { this.mockMvc.perform(get("/getAllEmployees")) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().string("[{\"id\":1,\"firstName\":\"Foo\",\"lastName\":\"Bar\",\"country\":\"Sweden\"}]")); } }
Run the test:
$ mvn test -Dspring-boot.run.profiles=test
Obviously this fails because we do not have a running database:
2020-04-04 13:54:28.652 ERROR 6609 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization. org.postgresql.util.PSQLException: Connection to localhost:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections. ... Caused by: java.net.ConnectException: Connection refused (Connection refused) ...
The solution for this problem is using Testcontainers.
3. Use Testcontainers
We need a PostgreSQL database in order to run the integration test we wrote. We can do so by using the Spring Boot Testcontainers. In order to use these, we need to insert the spring-cloud-starter
, embedded-postgresql
and the junit-jupiter
testcontainers dependencies to our pom
.
<dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.13.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> <version>2.2.2.RELEASE</version> <scope>test</scope> </dependency> <dependency> <groupId>com.playtika.testcontainers</groupId> <artifactId>embedded-postgresql</artifactId> <version>1.43</version> <scope>test</scope> </dependency>
We add a file test/resources/bootstrap-test.properties
where we enable the embedded PostgreSQL database and where we define the Docker image to be used and the database access credentials.
embedded.postgresql.enabled=true embedded.postgresql.dockerImage=postgres:latest embedded.postgresql.user=postgres embedded.postgresql.password=root
We also move the application-test.properties
we created in our previous post to the test/resources
directory. We change the datasource
properties in order to use the properties as we configured in the bootstrap-test.properties
file. The ${embedded.postgresql.schema}
property is defaulted to test_db
.
spring.datasource.url=jdbc:postgresql://${embedded.postgresql.host}:${embedded.postgresql.port}/${embedded.postgresql.schema} spring.datasource.username=${embedded.postgresql.user} spring.datasource.password=${embedded.postgresql.password} spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml spring.liquibase.contexts=test
Last thing to do is to add the Testcontainer to our integration test. This only requires us to add the @Testcontainers
annotation.
@SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc @Testcontainers class MyliquibasePlanetApplicationTests { @Autowired private MockMvc mockMvc; @Test void contextLoads() { } @Test public void getAllEmployees() throws Exception { this.mockMvc.perform(get("/getAllEmployees")).andDo(print()).andExpect(status().isOk()) .andExpect(content().string("[{\"id\":1,\"firstName\":\"Foo\",\"lastName\":\"Bar\",\"country\":\"Sweden\"}]")); } }
Run the test again:
$ mvn test -Dspring-boot.run.profiles=test
Our integration test now succeeds successfully.
4. Create addEmployee Integration Test
In order to complete the integration test, we also add an integration test for the addEmployee
REST interface. We want to validate whether the employee is really added to the database. We therefore extend the EmployeeDAO
with a getEmployee
method:
public Employee getEmployee(final String firstName, final String lastName, final String country) { return jdbcTemplate.queryForObject("SELECT * FROM EMPLOYEE WHERE FIRST_NAME = ? AND LAST_NAME = ? AND COUNTRY = ?", new EmployeeRowMapper(), firstName, lastName, country); }
The addEmployee
integration test is the following:
@Autowired private JdbcTemplate jdbcTemplate; @Autowired private EmployeeDao employeeDao; ... @Test @Transactional public void addEmployee() throws Exception { int rowsBefore = JdbcTestUtils.countRowsInTable(jdbcTemplate, "Employee"); this.mockMvc.perform(post("/addEmployee") .param("firstName", "John") .param("lastName", "Doe") .param("country", "Belgium")) .andDo(print()) .andExpect(status().isOk()) .andExpect((content().string("Saved Employee"))); int rowsAfter = JdbcTestUtils.countRowsInTable(jdbcTemplate, "Employee"); Employee result = employeeDao.getEmployee("John", "Doe", "Belgium"); Assert.notNull(result, "No result available"); Assert.isTrue(rowsBefore + 1 == rowsAfter, "Employee is not added"); }
We inject the employeeDAO
because we want to verify whether the employee is added to the database in Line 24. The test is annotated with @Transactional
, this will ensure that the database changes during the test are rollbacked after the test. If we do not add this annotation, the getAllEmployees
integration test would fail when it is executed after the addEmployee
test (because of an extra record in the database). We use JdbcTestUtils
and the injected jdbcTemplate
to verify the number of rows before and after the REST call.
5. Conclusion
We have been using Testcontainers in order to execute some integration tests. Using Testcontainers is fairly easy and it gives us the opportunity to create integration tests without the need of pre-installed components. We just add a Testcontainer which is started at the beginning of the integration test and use the component from within the running integration tests.
If you added https://github.com/testcontainers/testcontainers-spring-boot
then the code below is not required:
@Container
static PostgreSQLContainer postgreSQL = new PostgreSQLContainer();
@DynamicPropertySource
static void postgreSQLProperties(DynamicPropertyRegistry registry) {
registry.add(“spring.datasource.url”, postgreSQL::getJdbcUrl);
registry.add(“spring.datasource.username”, postgreSQL::getUsername);
registry.add(“spring.datasource.password”, postgreSQL::getPassword);
}
LikeLike
Hello Ivan,
thank you very much for your comment! It is indeed not necessary and I updated the post accordingly. I think I was mislead by the Spring post https://spring.io/blog/2020/03/27/dynamicpropertysource-in-spring-framework-5-2-5-and-spring-boot-2-2-6 but at the end it says ‘it should be useful in any Spring-based integration test where a property’s value isn’t known up front’. The database connection properties are known up front.
Cheers,
Gunter
LikeLiked by 1 person
Added your article into readme if you don’t mind: https://github.com/testcontainers/testcontainers-spring-boot#in-depth-guides-how-tos-and-samples
LikeLiked by 1 person
I certainly don’t mind, I feel honored
LikeLike
If this is an integration test – we are trying to connect to real systems. Why are you using mockMvc? Should you not use RestTemplate?
LikeLiked by 1 person
That is a very good question. We just want to test the server side, so there is no need to spin up an actual servlet container for this test, which makes it still a valid integration test. If we have code using RestTemplate, then it would make sense to use RestTemplate. In that case, we would need a running server. More info can be found at (a rather old article, but still valid): https://spring.io/blog/2012/11/12/spring-framework-3-2-rc1-spring-mvc-test-framework
So with mockmvc we have a valid integration test and it will be faster than using RestTemplate.
LikeLiked by 1 person
Thank you for the link. I read the link you provided and this makes me believe even more that with mockMvc it is not an integration test rather a unit test. Please see these paragraph in the blog post link.
“What if you could re-write these controller unit tests but instead of invoking controllers directly, it would be done through the DispatcherServlet, just as it happens at runtime? And what if you could use a fluent API to specify the request to perform and the response you expect? All of that without the need for a servlet container.”
If you see the creators are saying this is a unit test – but call gets routed through dispatcher servlet like runtime and without servlet conainer. Test context does the magic behind the scenes.
It makes think – is with mock and no real server this would qualify as a unit test of the controller, albeit more comprehensive because we are able to test the annotations and request mappings as well. Even the services are mocked.
I believe – if we run with Restassured/RestTemplate/NewMan with containers hosting real services connected to integration test DB is a more accurate representation of integration test.
LikeLiked by 1 person
Nice article.
LikeLiked by 1 person
Thank you very much!
LikeLiked by 1 person