Testing Spring Boot Applications with Testcontainers and JUnit

Last Updated:  July 3, 2022 | Published: June 9, 2018

Testcontainers is a game-changing library for writing integration and end-to-end tests. With the advent of Docker, we can conveniently start infrastructure components like databases and messaging brokers without much effort on our machines. This helps to get our Spring Boot integration test setup as close to production as possible. With Testcontainers, we can manage the lifecycle of throwaway containers for testing purposes.

With this blog post, you'll get an introduction to Testcontainers and learn how to write integration tests for your Spring Boot application using this library.

Testcontainers Setup For Testing Spring Boot Applications

As a prerequisite for using Testcontainers, we have to ensure a Docker engine is running on our machine and available on our CI server:

Next, for Maven projects, we include the basic Testcontainers library as the following:

For Gradle-based projects, it's the following line:

For projects where we have multiple dependencies from Testcontainers, we must align their versions to avoid incompatibilities. Testcontainers provides a Maven Bill of Materials (BOM) for this purpose. Once we define the testcontainers-bom as part of Maven’s dependencyManagement section, we can include all Testcontainers dependencies without specifying their versions. The BOM will align all Testcontainers dependency versions for us:

Using Gradle, we include the BOM as the following:

Testcontainers also comes with first-class support for the following test frameworks: JUnit 4, JUnit Jupiter, and Spock.

Starting and Stopping a Container with Testcontainers

Let’s assume our application uses Keycloak as an identity provider. When writing end-to-end tests, we need a running instance of this identity provider.

With Testcontainers, we can now define a GenericContainer as part of our test setup.

The constructor of this GenericContainer expects and Docker image. For our example, we’ll use the official jboss/keycloak image:

With just this container definition, Testcontainers will not recognize this field definition as a Docker container to start (yet).

Using JUnit Jupiter, we can register the Testcontainers extension with @Testcontainers. Next, we have to identify all our container definitions with @Container:

With this setup, Testcontainers will now start our Keycloak Docker container before executing the first test. All test methods inside this test class will share this container.

For more fine-grained control of when to start and stop the container, we can use the manual container lifecycle control:

Configure the Docker Container with Testcontainers

With this in setup place, how can Testcontainers know when our container is ready to receive traffic? Right now, it doesn’t.

That’s where the WaitStrategy of Testcontainers comes into play. There are multiple wait strategies available:

  • wait for a port to be available
  • wait for an HTTP request to return a specific status code
  • expect a value as part of the container’s log output

For our Keycloak instance, we can use the admin UI path /auth as a readiness identifier.

Once this endpoint returns the HTTP status code 200, we can be sure our container started successfully:

Next, we can further tweak our container definition:

  • define which ports to expose
  • add environment variables
  • map resources from the classpath to our container
  • … and much more

For our Keycloak example, we can expose port 8080 and define the credentials for the Keycloak admin user as part of the environment:

Upon test execution, Testcontainers will pick a random ephemeral port for any of our exposed ports to avoid port clashes.

When running a test class with the container setup above, we’ll see the following two containers on our machine:

The second Ryuk container acts as a helper for housekeeping tasks (deleting container/volumes/networks) after the test.

Once the container is up and running, we can retrieve the mapped system port and interact with the container. This allows us to, e.g., retrieve the stdout/stderr from the container, execute commands inside the container, etc.:

At the end of the test Testcontainers, will ensure to stop and remove our containers. This happens either right before the JVM exits or as part of @AfterAll when using the JUnit Jupiter extension. This housekeeping ensures we don’t have any running containers on our machine.

Pretty impressive, isn’t it?

Using Modules From Testcontainers

With the GenericContainer, we can go pretty far and tweak our container definition to our needs. However, we might find ourselves repeatedly doing the same container definition for multiple projects.

Fortunately, Testcontainers provides a set of modules for convenient bootstrapping of common Docker containers. The list of available modules is enormous (especially for databases). There are ready-to-use modules available for Kafka, PostgreSQL, Webdriver, RabbitMQ, Elasticsearch, etc.

If we don't find ourselves lucky to use one of these modules, we can also create our own and share it inside our cooperation or publicly. Furthermore, excellent community Testcontainers modules are available, like Wim Deblauwe's Cypress module or a Keycloak module from Niko Köbler.

For demonstration purposes, let's use the PostgreSQL module to test our project's persistence layer. We can include any Testcontainers module as an additional dependency:

This dependency includes the PostgreSQLContainer class for a convenient PostgreSQL database definition. Depending on which module we use, the container classes provide additional helper methods to set up the container easily.

In the PostgreSQLContainer example, we use .withUsername() to define PostgreSQL's admin user and don't have to remember any environment variables or configuration files that make this happen:

Now it's time to integrate Testcontainers for our Spring Boot integration tests.

Spring Boot Integration Tests: Testcontainers and JUnit 4

Using: JUnit 4.12 and Spring Boot < 2.2.6

For demonstration purposes, let's assume we want to write an integration test for our Spring Boot application that connects to a PostgreSQL database. We use the Testcontainers PostgreSQL module to avoid any manual GenericContainer definitions.

As our application expects a running PostgreSQL database upon application startup (to create the DataSource), we define a PostgreSQLContainer for our test. With the help of Testcontainers JUnit 4 support, the @ClassRule ensures to start our database container before the Spring context launches.

Testcontainers maps the PostgreSQL's main port (5432) to a random and ephemeral port, so we must override our configuration dynamically.

For Spring Boot applications < 2.2.6, we can achieve this with an ApplicationContextInitializer and set the connection parameters dynamically:

With this setup in place, we can now start the test and see a successful execution.

For a hands-on demonstration for this setup, consider watching this video.

Spring Boot Integration Tests: Testcontainers and JUnit 5

For applications that use JUnit Jupiter (part of JUnit 5), we can't use the @ClassRule anymore. The extension model of JUnit Jupiter exceeds the rule/runner API from JUnit 4.

As already outlined in the Testcontainers introduction section, there's support for JUnit Jupiter available:

With this dependency and a more recent version of Spring Boot (> 2.2.6), the same integration test looks like the following:

With the help of @DynamicPropertySource we can dynamically override the datasource connection parameters.

If our application uses a Spring Boot version before 2.2.6, we don't have access to the @DynamicPropertySource feature. For such applications, we can fallback to the ApplicationContextInitializer setup approach:

Further Content On Testcontainers

For more information and examples on using Testcontainers, consider the following articles:

Also, check out my YouTube channel for hands-on Testcontainers examples.

The source code for this Testcontainers introduction for Testing Spring Boot Applications is available on GitHub.

As part of the Testing Spring Boot Applications Masterclass, we make heavy use of Testcontainers to test a real-world Spring Boot application. We'll also do a deep-dive on Testcontainers and how to use some of its advanced features (starting a Docker Compose environment, reusable containers, etc.) for writing integration and end-to-end tests.

Happy integration-testing with Spring Boot, Testcontainers, and JUnit,

Philip

  • […] Ensuring your application is working properly is a critical part of continuous integration and delivery. Unit tests help you to test your methods’ business logic for both normal & edge cases. When it comes to guaranteeing that your users are able to correctly work with your application, we need something different. If your application exposes a frontend with user interaction, we can write functional tests to ensure different use cases are working. With this blog post, I’ll provide an example of how to write Functional Tests for Spring Boot applications using Selenium and Testcontainers. […]

  • Do you have any idea why the integration test IndexControllerIT does not run when I execute mvn verify? Only the ApplicationTest is executed. I suppose Maven’s integration test phase is not executed. I use Maven 3.6.3, if that is important.

    • Hi Jesper,

      did you include the Maven Failsafe Plugin in your project? The Surefire Plugin usually runs unit tests and the Failsafe plugin the integration test. It’s not part of Maven’s default plugins and hence needs to be manually added. You can find detailed information here.

      Let me know if that helps,
      Philip

  • Thanks very much for this very informative article and, especially, for keeping it updated. Several top ranking Google search results for using Testcontainers in Spring Boot projects are using Junit 4 which doesn’t work for that later versions of Spring Boot. I spent hours trying to figure out why the Spring context was not being loaded prior to executing my @DataJpaTest which had the PostgreSQLContainer attempted to be loaded via @ClassRule… Happy I came your article

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >