Kotest + Spring + Testcontainers

Recently, I wanted to rewrite a JUnit integration test for my Spring Boot application. Since it was an integration test, I used Testcontainers to start a database. While there are Kotest extensions for Spring and Testcontainers we have the problem that both do not cooperate.

The Testcontainers extension does not integrate with Spring in a way that ensures the container starts early enough for Spring and overrides the Spring properties (e.g., spring.data.jdbc.url).

While searching for a solution, I came across several articles that seemed somewhat outdated. Therefore, I wanted to share my solution here. The articles I found were:

What all of those solutions have in common is that they wrap the container and then control its lifecycle. This could be achieved through extension functions, static methods, or an abstract test superclass. Having all the lifecycle callbacks, you can then start and stop the container before the Spring application context starts and override the properties.

Although these approaches work, they seemed unnecessarily complex to me.

Starting point

What I started with was a common Spring JUnit test with Testcontainers:

@ActiveProfiles("default", "test")
@Testcontainers
@SpringBootTest
class SomeIntegrationTest() {
    companion object {
        @Container
        private val postgres = PostgreSQLContainer("postgres:16.1")

        @JvmStatic
        @DynamicPropertySource
        fun properties(registry: DynamicPropertyRegistry) {
            with(registry) {
                add("spring.flyway.url") { postgres.jdbcUrl }
                add("spring.flyway.user") { postgres.username }
                add("spring.flyway.password") { postgres.password }
                add("spring.r2dbc.url") {
                    postgres.jdbcUrl.replaceFirst("jdbc", "r2dbc")
                }
                add("spring.r2dbc.username") { postgres.username }
                add("spring.r2dbc.password") { postgres.password }
            }
        }
    }
// setup, teardown and tests go here
...
}

While I could keep the @SpringBootTest annotation due to the Kotest extension @Testcontainers , which helps with the container lifecycle, does not work with Kotest.

The Kotest version

What my predecessors did not have at their time was the @ServiceConnection annotation. You can read more about it here. Given a standard Postgres container, it should be possible to use this annotation. So, I experimented and rewrote the test like this:

import io.kotest.extensions.testcontainers.perSpec

@ActiveProfiles("default", "test")
@SpringBootTest
class SomeIntegrationTest : StringSpec() {

    companion object {
        @ServiceConnection
        private val postgres = PostgreSQLContainer("postgres:16.1")
            .apply { this.start() }
    }

    init {
        listener(postgres.perSpec())
        // setup, teardown and tests go here
        ...
    }
}

I also encountered the issue where the container started too late for Spring. To address this, I start the container explicitly in the companion object. Registering the container as a listener using perSpec() then managed the rest of the lifecycle.

With spring-boot-testcontainers in the classpath, there’s no need to override properties manually anymore. We can just add the @ServiceConnection annotation.

Conclusion

So, all in all, instead of having to write our own lifecycle wrapper we just use perSpec() and the manual start with .apply { this.start() } and have a nicely working integration test. The @ServiceConnection annotation could also have been applied to the original test, but it’s great to see that it works seamlessly with the Kotest Spring extension.

0
Subscribe to my newsletter

Read articles from Ronny Bräunlich directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ronny Bräunlich
Ronny Bräunlich