Skip to main content

Kontainers with JUnit Jupiter

Kontainers integrates with JUnit using JUnit Jupiter extensions. These extensions enable Kontainers to easily used with JUnit for integration testing.

Kontainers can be injected into tests as a constructor parameters, which is useful in cases where you need access to the running Kontainer.

Kontainers can also be used simply as properties suppliers to integration tests via another set of annotations.

Usage

Both extensions are available as a single dependency:

build.gradle.kts
dependencies {
testImplementation("io.microkt.kontainers:kontainers-junit5"))
}

JUnit Parameter Extension

To use Kontainers are constructor parameters, annotate your test suite with the @Kontainers annotation and pass the Kontainer(s) you'd like to use to the constructor.

The Kontainers JUnit extension will manage your Kontainer's lifecycle for you. It will be started before your tests are run and torn down after tests complete, regardless of test failures.

@Kontainers
internal class MysqlKontainerTest(private val mysql: MysqlKontainer) {
private var dataSource: DataSource = buildDataSource(mysql)

@Test
fun testQuery() {
val statement: Statement = dataSource.connection.createStatement()
statement.execute("SELECT 1")

val resultSet = statement.resultSet
resultSet.next()

val resultInt = resultSet.getInt(1)
assertEquals(1, resultInt, "SELECT 1; should return 1")
}

companion object {
private fun buildDataSource(jdbcKontainer: JdbcKontainer): DataSource =
HikariDataSource(
HikariConfig().apply {
jdbcUrl = jdbcKontainer.createJdbcUrl()
username = jdbcKontainer.getUsername()
password = jdbcKontainer.getPassword()
}
)
}
}

Customizing Parameter Kontainers

Using the JUnit 5 plugin, you can customize the Kontainer using the annotation @KontainerSpecOverride to update the KontainerSpec used to create the Kontainer.

@KontainerSpecOverride can be used to override values on each Kontainer constructor parameter. For example, if your integration test needs Redis and Localstack, you may want to apply customizations to each Kontainer to meet your requirements. You may need to override the Redis image, while also customizing which AWS services are started by LocalStack.

MyIntegrationTest.kt
internal class RedisBusterSpecProvider : KontainerSpecProvider {
override fun override(kontainerSpec: KontainerSpec): KontainerSpec =
kontainerSpec(kontainerSpec) {
image = "redis:6.2-buster"
}
}

internal class LocalstackSpecProvider : KontainerSpecProvider {
override fun override(kontainerSpec: KontainerSpec): KontainerSpec =
kontainerSpec(kontainerSpec) {
environment {
set("SERVICS" to "sqs")
}
}

@Kontainers
internal class MyIntegrationTest(
@KontainerSpecOverride(LocalstackSpecProvider::class)
private val localstack: LocalstackKontainer
@KontainerSpecOverride(RedisBusterSpecProvider::class)
private val redis: RedisKontainer
) {
// tests here
}

JUnit Property Supplier Extensions

Property supplier Kontainers are useful when you want to expose properties to an integration test, but you don't need to reference the underlying Kontainer instance in your tests.

A good example of such use cases are the Spring Boot and Micronaut integrations.

Generic Kontainer

For non-relational database containers, the most appropriate property supplier annotation is @Kontainer. @Kontainer requires a single value, the class of the Kontainer to run.

For example, to run Redis:

IntegrationTest.kt
@Kontainer(RedisKontainer::class)
internal class IntegrationTest {
// tests
}

Using Property Suppliers

By default, property suppliers are provided for common framework properties that are applicable to the Kontainers run with a @Kontainer annotation. However, you may supply your own properties by writing a property supplier.

For example, if you'd like to export a property named redis.uri containing the URI to the running Redis Kontainer, you can write a property supplier.

MyRedisPropertySupplier.kt
class MyRedisPropertySupplier : PropertySupplier {
override fun supply(kontainer: Kontainer): Map<String, String> =
when (kontainer) {
is RedisKontainer -> redisProps(kontainer)
else -> mapOf()
}

private fun redisProps(kontainer: RedisKontainer): Map<String, String> =
mapOf(
"redis.uri" to "redis://${kontainer.getAddress()}/${kontainer.getPort()}"
)

Then use your property supplier in your integration test:

IntegrationTest.kt
@Kontainer(
RedisKontainer::class,
propertySuppliers = [MyRedisPropertySupplier::class]
)
internal class IntegrationTest {
@Test
fun testProps() {
assertNotNull(System.getProperty("redis.uri"))
}
}

Database Kontainer

@DatabaseKontainer is semantically similar to Generic Kontainer, however it provides JDBC and R2DBC specific properties to integration tests. @DatabaseKontainer should be preferred when using containerized databases because it has more reliable support for determining when a database is ready to accept client connections.

Similarly to @Kontainer, you can use your own custom property suppliers.

Sample using PostgreSQL with Spring Boot:

DemoApplicationTests.kt
@SpringBootTest
@DatabaseKontainer(PostgresKontainer::class)
class DemoApplicationTests {
@Autowired
private final lateinit var animalRepository: ReactiveAnimalRepository

@Test
fun contextLoads() = runBlocking {
assertEquals("dog", animalRepository.findByName("dog").awaitFirst().name)
}
}