Are you struggling finding the right strategy for your integration tests? Mocking does not cover everything and relying on all external services to be present also gives you a headache. What if there was a way to start the real service in your integration tests? In comes testcontainers.
For more information on configuration and more examples, take a look at the testcontainers website.
We will start off with a Spring Boot API from this previous post on deployments with helm. We will extend this application with a database connection and use testcontainers in the integration tests. The complete code for this post can be on github here.
Application maintenance
This could sound familiar but the starting application has not been worked on for a long time. We first have to update some things to get started. First I updated the maven dependencies found in the pom. But the main upgrade is to Helm 3. We first remove the Helm 2 deployment
helm -n sybrenbolandit delete spring-api
And now upgrade helm to the last version with homebrew.
brew upgrade helm
If we now try to deploy our application we see that we have a deprecation warning.

This could be ignored until version v1.22 but let’s upgrade the Ingress definition to the latest API specification.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}
labels:
{{ include "java-spring-api.labels" . | nindent 4 }}
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
rules:
- http:
paths:
- path: /{{ or $.Values.ingress.path $.Release.Name }}/(.*)
pathType: Prefix
backend:
service:
name: {{ include "java-spring-api.fullname" $ }}
port:
number: 8080
Now the application can be deployed with Helm 3 without any problems.

Adding a database connection
We will now add a new feature to our application: The objects will be saved in a database! Spring has great guides that will tell you all about this, like this one on Spring Data JPA. We will follow along by creating an @Entity object together with a CrudRepository. The only difference is we will connect to a MySQL database. Here is the configuration.
################################################################################
# DATABASE
################################################################################
spring.datasource.url=jdbc:mysql://localhost:3306/my_database
spring.datasource.username=root
spring.datasource.password=<secret>
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
Note that the secret will be known when we instantiate a database.
The integration test problem
To cover the critical flows of our application we want to create integration tests. For example we want to test that a Person can be created. Here is the first iteration of such a test.
@SpringBootTest
public class PersonIT {
@Autowired
private PersonController personController;
@Test
public void testCreatePerson() {
Person person = Person.builder()
.name("Syb")
.email("syb@dev.com")
.build();
Person responsePerson = personController.createPerson(person);
assertEquals("Syb", responsePerson.getName());
assertEquals("syb@dev.com", responsePerson.getEmail());
}
}
Looks good. But if we try to run this test we get the following error.

Surely this connection is refused for there is no database to connect to… How to facilitate this connection is the integration test problem and all solutions have their pros and cons. Here we try a naive solution and a solution with testcontainers.
Note that there is also a solution with mocks. This is usually done by replacing a specific class/bean in the application, here the repository class would be a candidate, and controlling its behavior. Although this is a fairly good solution to test the business logic, it does not cover an actual connection to an external service. We will skip the use of mocks for now.
A naive solution
A naive approach would be to just start up a database to connect too. Sadly what is still the norm in a lot of companies. This means developers have to manage their own database which means the database is different in different environments.
A slightly better approach is to use a standard deployment. Here we will use the Helm deployment of stable Helm charts to deploy a standard MySql database.
helm -n sybrenbolandit install \
--set mysqlDatabase=person \
--set imageTag=8.0.26 \
mysql stable/mysql
Note that there is a lot of information in the deployment output. Like where the service is available, how to run a client pod and how to retrieve the password set in a Kubernetes Secret.
kubectl get secret --namespace sybrenbolandit mysql -o jsonpath="{.data.mysql-root-password}" | base64 --decode; echo
Copy the password to your application.properties and run the application to test the connection. Be sure to create a port-forward to the database.
kubectl -n sybrenbolandit port-forward svc/mysql 3306:3306
With this port forward we can also run the integration tests. These will pass but we trust that an external database is there all the time. This is not really reliable.

A testcontainers solution
Luckily there is another solution. With testcontainers we can spin up every container needed for a specific test. Here is an example of how this looks.
@Container
public GenericContainer service = new GenericContainer("myservice:1.2.3");
We see that we input the image name inside the GenericContainer class. This suggests that there are more classes for specific containers. And for a lot of common images there are modules available with an API specific for that image. The menu of the testcontainers website gives a great overview of all the modules available. For our MySQL database there is a class ready to go.
@Container
public static MySQLContainer mysql = new MySQLContainer("mysql:8.0.26");
Note that we added the static keyword to the declaration. This way the container is kept running for all tests in the same test class. In our first example the myservice would restart for every test.
To make this all work we need to do some general preparation. Like adding some dependencies.
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
After the core dependency of testcontainers we add the jUnit5/jupiter dependency. If you are using jUnit4 or spock there are dependencies for these also. Furthermore we need the jUnit5 dependencies themselves but I will omit the for Spring Boot provides them in this example. Also note that the mysql dependency makes the MySQL module of testcontainers available.
The last thing to do is to annotatie the test class with the @Testcontainers annotation.
@SpringBootTest
@Testcontainers
public class PersonIT {
...
When we try to run our tests we again get the connection refused error. We look in the logs to find the following.

Hey! The database url that testcontainers uses is not the one we defined in our application.properties. Testcontainers picks a random port because multiple tests or test classes will all run their own containers which could conflict. We have to adjust our properties after the container is started.
We could use implement a ApplicationContextInitializer<ConfigurableApplicationContext> like in this testcontainers tutorial on bealdung. But Spring has another handy way of dynamically updating the properties.
@DynamicPropertySource
public static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
Now the properties are used from the started container. Run the tests again to see them succeed.

There are many more features of testcontainers like accessing container logs, mounting files, executing commands and networking. This will be input for future posts and can be studies now at the testcontainers website.
Hopefully you are now able to start your own external services in your Spring Boot integration tests. Happy testcontainering!