Testing Spring Boot Applications with Kotlin and Testcontainers

Last Updated:  April 6, 2022 | Published: August 17, 2020

Recently I introduced Testcontainers for a Kotlin based Spring Boot application. I jumped right into the setup and immediately ran into an issue. Even though the interoperability of Java libraries and Kotlin is excellent, there is one thing you need to know when using Testcontainers with Kotlin. Continue reading to understand what it takes to use Testcontainers for Kotlin projects using Spring Boot in specific.

Kotlin and Spring Boot project setup

The demo project uses a Spring Boot project generated by start.spring.io. It includes all dependencies we need to write our Spring Boot application with Kotlin and Spring Boot Starters for Web, Webflux, and JPA.

In addition, we use PostgreSQL as a database for which we include also the JBDC driver and Flyway to migrate our schema scripts:

Furthermore, as there is no dedicated Kotlin dependency for Testcontainers (yet),  we can use the same import as in a Java project:

While there is no big difference in the project setup when using Testcontainers with Kotlin and Spring Boot, once you want to create your first container, you run into an issue.

The initial problem when using Testcontainers with Kotlin

The problem lies in how the generic type is defined in the Java class of the Testcontainers library. Let's take a look at the PostgreSQLContainer as an example (the same applies to any other container class like GenericContainer):

That's a recursive generic type definition. In Java we usually ignore (read: accept the IDE/compiler warning: raw use of parameterized class) it when defining a container from Testcontainers:

Unfortunately, when porting the Java code above to Kotlin the class does not compile:

More information can be found inside the original GitHub issue and the corresponding bug ticket for Kotlin.

Workarounds for Testcontainers with Kotlin and Spring Boot

To tackle this issue, we have two options.

We can define a subclass of PostgreSQLContainer and to specify the generic type:

Or we can use Kotlin's Nothing as the generic type:

Next, we need the correct place inside our test class to make use of the Testcontainers extension to start and stop our container definitions.

As there is no static keyword in Kotlin, we can achieve the same while using a companion object:

The scoping function apply of Kotlin plays here quite nicely. It allows us to provide additional setup code inside a function block that gets the container instance passed as a receiver parameter.

You can also take over the control and manage the lifecycle of the containers for yourself. Therefore, remove the @Testcontainers annotation (that registers a JUnit 5 extension under the hood) and start the container inside the apply function:

There is no need to explicitly stop the container, as the Ryuk container automatically shuts it down after the test class.

Using @DynamicPropertySource with Kotlin

There is one missing piece to now make it work with Spring Boot: How do we apply the dynamic JDBC URL to our context?

As already outlined in a previous blog post, there are multiple solutions to this. If you are one of the lucky developers that already use Spring Boot > 2.2.6 for your project, you can achieve it with the following:

Don't forget to add the @JvmStatic to the function inside your companion object. Otherwise, Spring Test won't detect it and your test will fail.

Finally, we can now glue it all together and write a test to ensure our application can start and we are able to access a public endpoint:

That's everything you need to make Testcontainers for a Kotlin Spring Boot application work. Any other Testcontainers features should be now applicable to your Kotlin test.

You can find more content about Testcontainers here and the demo project on GitHub.

Have fun using Testcontainers with Kotlin and Spring Boot,

Phil

  • Hi,
    Nice article, I’m trying to do something similar but since I want to unit test my repositories I want to create a container to be used in several repository classes. Do you have any solution for that?

    • Hey Miguel,

      if you want for example to only start one database container for all your repository tests, you can introduce an abstract class that only includes the container definition and a static block to start it. All your repository tests can then extend this class and the database is only started once.

      This concept is explained in the Testcontainers docs here using a Java example which you can convert to Kotlin.

      Let me know if that solves your problem.

      • Hi rieckpil,
        Thank you for your answer, this is what I was trying to do but without success:

        @DataJpaTest
        @Testcontainers
        @Import(value = [MapperConfig::class, AuditConfig::class])
        @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
        abstract class BasePostgreSQLContainer {

        companion object {

        @JvmStatic
        @Container
        val container = PostgreSQLContainer("postgres:latest").apply {
        withReuse(true)
        }

        @JvmStatic
        @DynamicPropertySource
        fun postgresProperties(registry: DynamicPropertyRegistry) {
        // ...
        }
        }
        }

        • Hey Miguel,

          thanks for your response. To manually manage the lifecycle and use singleton containers, you can’t use @Testcontainers as this registers a JUnit 5 extension that manages the lifecycle for you.

          So try to remove @Testcontainers and @Container and start your container manually inside your static code block using .start(). Your container will be automatically removed once the JVM (if you don’t opt-in for reusable containers) for your unit tests exists or you call .stop().

          Hope this helps,
          Philip

  • I am struggling to download the image from my private docker repo. The documentation is unclear in TestContainer docs so can you please help with that?

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