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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>de.rieckpil.blog</groupId> <artifactId>spring-boot-kotlin-testcontainers</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-kotlin-testcontainers</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> <kotlin.version>1.3.72</kotlin.version> <testcontainers.version>1.14.3</testcontainers.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-reflect</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <!-- Testcontainers dependencies --> </dependencies> <build> <!-- build section with Kotlin compiler settings --> </build> </project> |
Furthermore, as there is no dedicated Kotlin dependency for Testcontainers (yet), we can use the same import as in a Java project:
1 2 3 4 5 6 7 8 9 10 11 12 | <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</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> |
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
):
1 | public class PostgreSQLContainer<SELF extends PostgreSQLContainer<SELF>> extends JdbcDatabaseContainer<SELF> |
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:
1 | static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer(); |
Unfortunately, when porting the Java code above to Kotlin the class does not compile:
1 2 | // Kotlin: One type argument expected for class PostgreSQLContainer<SELF : PostgreSQLContainer<SELF!>!> val container: PostgreSQLContainer = PostgreSQLContainer() |
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:
1 | internal class KPostgreSQLContainer(image: String) : PostgreSQLContainer<KPostgreSQLContainer>(image) |
Or we can use Kotlin's Nothing
as the generic type:
1 | val container = PostgreSQLContainer<Nothing>() |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Testcontainers @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ApplicationTest { companion object { @Container val container = PostgreSQLContainer<Nothing>("postgres:12").apply { withDatabaseName("testdb") withUsername("duke") withPassword("s3crEt") } } } |
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:
1 2 3 4 5 6 7 8 | companion object { private val container = KPostgreSQLContainer("postgres:12").apply { withDatabaseName("testdb") withUsername("duke") withPassword("s3crEt") start() } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 | companion object { // ... container definition @JvmStatic @DynamicPropertySource fun properties(registry: DynamicPropertyRegistry) { registry.add("spring.datasource.url", container::getJdbcUrl); registry.add("spring.datasource.password", container::getPassword); registry.add("spring.datasource.username", container::getUsername); } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | @Testcontainers @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ApplicationTest { @Autowired private lateinit var bookRepository: BookRepository @Autowired private lateinit var webTestClient: WebTestClient companion object { @Container val container = PostgreSQLContainer<Nothing>("postgres:12").apply { withDatabaseName("testdb") withUsername("duke") withPassword("s3crEt") } @JvmStatic @DynamicPropertySource fun properties(registry: DynamicPropertyRegistry) { registry.add("spring.datasource.url", container::getJdbcUrl); registry.add("spring.datasource.password", container::getPassword); registry.add("spring.datasource.username", container::getUsername); } } @Test fun shouldReturnAllBooks() { this.bookRepository.save(Book(1L, "42", "Java 14")) this.bookRepository.save(Book(2L, "84", "Java 7")) this.webTestClient.get() .uri("/books") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().is2xxSuccessful .expectBody().jsonPath("$.length()").isEqualTo(2) .jsonPath("$[0].id").isEqualTo(1) .jsonPath("$[0].isbn").isEqualTo(42) .jsonPath("$[0].title").isEqualTo("Java 14") } } |
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