Integration Tests With Testcontainers
When we implement a new feature in our Spring Boot application that interacts with a database or other services, we first have to write tests. But unfortunately, it is not easy to do.
I have used the H2 database or SQLite in my integration tests before. I believe most of us do this way. Unfortunately, it doesn’t work in most cases because the SQL syntax of database engines may differ, and our integration tests may vary as well. The first solution to this problem is to start the same database as the one used in production. However, we have some disadvantages of this solution, and one of them is you have to ask your DBA to create the database or tables that start, for example, with the prefix “test_” or something like that, to run your tests in the build server. In some cases, it is not easy to approach.
But there is a good solution. If you have installed a docker on your build server and your computer, you can create a real environment and launch your tests, even in your Junit tests. The solution is the excellent tool called Testcontainers. It has many advantages like:
- You can launch the docker container (or database engine in docker if it has docker image) with a random port suitable for your tests won’t fail because of the port conflict.
- You don’t have to clear your database each time because Testcontainers launches the empty DB.
- You are not limited to use Testcontainers only for the database. You can include to your tests any docker from the docker hub. For these cases, you have to create with GenericContainer. This class has a constructor with a String parameter where you can pass your container name with its version like “dockerImageName:version”.
Let’s write some tests in Spring Boot. For instance, we have to test the DAO part of our application, like shown in the following. Here I am using Spring Data JPA and Hibernate as an ORM framework.
and we will have an entity class here as well:
Let’s assume we have to test this UserRepository
, and we want to test findByUsername
method. Actually we don’t have to test this method, because it is already well tested in Spring Data JPA I believe, but we will test it as an example.
First of all, we have to prepare database table called “User”. We could just set spring.jpa.hibernate.ddl-auto=update
, but in production we won’t set it to “update”. It should be “none”. So that is why tables would not be created by Hibernate in our tests too. We will create it with the DB Migration tool called Liquibase.
Below I have created changeset for MySQL.
I am not going to deep dive into creating changesets in Liquibase and configuring it in Spring Boot, because it is out of the scope of this topic. I don’t think that DB Migration tools are hard to learn. It is pretty straightforward if you are good at relational databases. Instead, we will focus on Testcontainers.
Let’s add Testcontainers BOM (Bill Of Materials) first:
and then Testcontainers and JUnit 5 dependency. Here we can omit the dependency version because we have added BOM already, and we will use the version from there.
And we are ready to start to write our integration test. The code can be downloaded from here.
We will need to add MySQL dependency for TestContainers as well:
Let’s write a test for findByUsername
method.
In line 18, we are launching Spring Boot application with a random port, obviously for not to fail when the port is in use.
- In line 19, we are configuring Spring Boot with our
Initializer
, which is theUserRepositoryTest
inner class in our case. - In the
Initializer
class, we are launching MySQL, and after launch, we are setting properties ininitialize
method. - In the
findByUsername
method, we are just creating two separate users, saving them to the database, and getting data from the database by id and byfindByUsername
method and asserting results.
Let’s launch the test.
$ mvn clean test
Voila! And it passes:
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 57.796 s - in com.example.demo.repository.UserRepositoryTest
2021-05-04 16:41:38.447 INFO 15805 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2021-05-04 16:41:38.448 INFO 15805 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2021-05-04 16:41:38.452 INFO 15805 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2021-05-04 16:41:38.476 INFO 15805 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:07 min
[INFO] Finished at: 2021-05-04T16:41:38+05:00
[INFO] ------------------------------------------------------------------------
$
Original post you can find in my dev blog https://www.asadganiev.com/Integration-tests-with-Testcontainers/