
Kotlin + JUnit 6
Introduction
For a long time, my team combined JUnit with kotlin-test because while JUnit offered more advanced features, such as parameterised tests, kotlin-test had more Kotlin-friendly assertions, such as assertNotNull that included contracts for nullability checks. However, recently we saw multiple improvements that make JUnit integrate with Kotlin better than ever before.
JUnit 6 comes with a long list of improvements, from architecture updates to better integration and performance. You can find the full list in the official release notes [1]. In this article, however, I will focus on three Kotlin-specific improvements – two from recent JUnit 5 releases and one from JUnit 6 – that may have flown under the radar:
- Contracts in assertions (since JUnit 5.12.0)
Sequence support for@MethodSource,@FieldSource, and@TestFactory(since JUnit 5.13.0)- Support for
suspend test methods (since JUnit 6, with official baselines of Java 17+ and Kotlin 2.2+)
In the next sections, I will demonstrate how these changes lead to simpler and more expressive Kotlin JUnit tests.
Contracts in assert functions
One of the most practical Kotlin-specific additions since JUnit 5.12.0 [4] is the new Kotlin-first top-level assertions in Assertions.kt. The key improvement: Kotlin contracts that enable smart casts after checks. For example, assertNotNull allows the compiler to smart cast the value to non-null after the call.
Before (JUnit 5.11.4):
import org.junit.jupiter.api.Assertions.assertNotNull
@Test
fun `should handle non-null value`() {
val result: String? = getValue()
assertNotNull(result)
// Compiler still thinks 'result' is nullable ❌
println(result!!.length)
}
fun getValue(): String? = "I am nullable"
A full runnable example of before code snippets is available on GitHub [7]
Now (from JUnit 5.12.0):
import org.junit.jupiter.api.assertNotNull
@Test
fun `should handle non-null value`() {
val result: String? = getValue()
assertNotNull(result)
// Compiler understands 'result' is non-null ✅
println(result.length)
}
fun getValue(): String? = "I am nullable"
A full runnable example of after code snippets is available on GitHub [8]
No more !! operator or mixing in kotlin-test asserts, just import org.junit.jupiter.api.assertNotNull instead of org.junit.jupiter.api.Assertions.assertNotNull.
These top-level functions make JUnit integrate with Kotlin more naturally. If you are writing Kotlin tests, prefer the top-level versions and only use org.junit.jupiter.api.Assertions.* for functions the top-level API does not expose.
Kotlin Sequence in parameter sources
Since version 5.13.0 [5], JUnit accepts Kotlin Sequence for parameter sources in @MethodSource, @FieldSource, and for @TestFactory. This is useful when you want lazy evaluation for parameters in parameterised tests.
@MethodSource
Consider @MethodSource parameterised test:
@ParameterizedTest
@MethodSource("ids")
fun `should load item`(id: String) {
assertTrue(id in setOf("A", "B", "C"))
}
Before JUnit version 5.13.0:
companion object {
// only: 'Stream<?>', 'Iterator<?>', 'Iterable<?>' or 'Object[]' ❌
@JvmStatic
fun ids(): Iterable<String> = listOf("A", "B", "C")
}
Starting from JUnit version 5.13.0:
companion object {
// Can use sequence ✅
@JvmStatic
fun ids(): Sequence<String> = sequenceOf("A", "B", "C")
}
@FieldSource
Consider @FieldSource parameterised test:
@ParameterizedTest
@FieldSource("names")
fun `should greet`(name: String) {
assertTrue(name.isNotEmpty())
}
Before JUnit version 5.13.0:
companion object {
// only: 'Stream<?>', 'Iterator<?>', 'Iterable<?>' or 'Object[]' ❌
@JvmField
val names: Iterable<String> = listOf("Kotlin", "JUnit")
}
Starting from JUnit version 5.13.0:
companion object {
// Can use sequence ✅
@JvmField
val names: Sequence<String> = sequenceOf("Kotlin", "JUnit")
}
@TestFactory
Before JUnit version 5.13.0 @TestFactory test:
// only: 'Stream<?>', 'Iterator<?>', 'Iterable<?>' or 'Object[]' ❌
@TestFactory
fun `should produce correct square`(): Iterable<DynamicTest> = squaresTestData()
.map { (input, expected) ->
DynamicTest.dynamicTest("when I calculate ${'$'}input^2 then I get ${'$'}expected") {
assertEquals(expected, calculator.square(input))
}
}.toList()
private fun squaresTestData(): Sequence<Pair<Int, Int>> = sequenceOf(
1 to 1,
2 to 4,
3 to 9,
4 to 16,
5 to 25
)
Starting from JUnit version 5.13.0 @TestFactory test:
// Can use sequence ✅
@TestFactory
fun `should produce correct square`(): Sequence<DynamicTest> = squaresTestData()
.map { (input, expected) ->
DynamicTest.dynamicTest("when I calculate ${'$'}input^2 then I get ${'$'}expected") {
assertEquals(expected, calculator.square(input))
}
}
private fun squaresTestData(): Sequence<Pair<Int, Int>> = sequenceOf(
1 to 1,
2 to 4,
3 to 9,
4 to 16,
5 to 25
)
Testing suspend functions
Another welcome improvement is direct support for Kotlin suspend functions added in JUnit 6 [1]. You can now mark test functions as suspend, and JUnit will run them without requiring runBlocking.
Before (JUnit 5):
@Test
fun `should fetch data`(): Unit = runBlocking {
val data = fetchData()
assertEquals("OK", data.status)
}
Now (JUnit 6):
@Test
suspend fun `should fetch data`() {
val data = fetchData()
assertEquals("OK", data.status)
}
This change makes coroutine testing feel idiomatic, consistent with how you write asynchronous code in production.
⚠️ Note: if you are using IntelliJ IDEA 2025.2.x, you will see validation errors on suspend tests in JUnit 6 even though they work correctly. This is a known IDEA bug (IDEA-379104 [3]) and has been fixed in IntelliJ IDEA 2025.3.x.
Does suspend support replace kotlinx-coroutines-test?
The short answer is no. JUnit 6’s suspend support has the same behaviour as runBlocking and is excellent for simple coroutine tests, but it does not replace the full feature set of the kotlinx-coroutines-test library. Use kotlinx-coroutines-test [2] when you need virtual time, deterministic scheduling, or dispatcher control.
Job Offers
Summary
Recent JUnit releases bring many changes, but for Kotlin developers, three are most notable:
- New top-level assert functions, including
assertNotNullthat has a Kotlin contract, so the compiler knows the value is safe to use after the check. - Support for sequences in parameterised tests enhances the performance of Kotlin tests.
- You can write
suspendtests directly, no morerunBlocking.
Together, these updates make Kotlin testing with JUnit feel more natural and concise. Full executable samples are available on GitHub.
For migration from older versions of JUnit, see the official JUnit 6 migration guide [6].
Note: JUnit runs on the JVM only and is therefore not suitable for Kotlin Multiplatform.
References
[1] https://docs.junit.org/6.0.0/release-notes/
[2] https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/
[4] https://docs.junit.org/5.12.0/release-notes/
[5] https://docs.junit.org/5.13.0/release-notes/
[6] https://github.com/junit-team/junit-framework/wiki/Upgrading-to-JUnit-6.0
[7] https://github.com/elenavanengelenmaslova/junit-6-kotlin-demo/tree/main/junit5
[8] https://github.com/elenavanengelenmaslova/junit-6-kotlin-demo/tree/main/junit6
If you found this useful, a coffee is always appreciated ☕ !
This article was previously published on proandroiddev.com



