Use whenever the task is to write, add, fix or review JVM unit tests in Java, Kotlin, Scala or Groovy: JUnit 6 (Jupiter) by default and JUnit 5 upgrades, plus Kotest, Spock, ScalaTest and TestNG. Covers parameterized tests, lifecycle hooks, Mockito, AssertJ, JaCoCo coverage and Maven or Gradle CI.
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Companion reference for jvm-unit-tests. Consult for Kotlin-only or
Kotlin-primary projects that want a Kotlin-idiomatic DSL over JUnit 5's
annotation-driven approach. For multi-language JVM projects, JUnit 5 is more
universal (SKILL.md).
Per kotest.io/docs:
Kotest differs from JUnit 5 by: multiple specification styles (DSL choice
per team), rich matchers (shouldBe, shouldContain, shouldThrow),
built-in property-based testing, coroutines-first test bodies, and
spec-level isolation modes. Matchers are bundled with the runner and are
not a drop-in standalone assertion library - for assertion-only use with
JUnit 5 / TestNG / Spock see assertj.md.
build.gradle.kts:
dependencies {
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
testImplementation("io.kotest:kotest-assertions-core:5.9.1")
testImplementation("io.kotest:kotest-property:5.9.1") // for property-based
}
tasks.test {
useJUnitPlatform()
}Per kt-docs, 8+ styles. Common picks - one style per project:
StringSpec (terse, no nesting):
class CalculatorTest : StringSpec({
"adds two numbers" {
Calculator.add(1, 2) shouldBe 3
}
"throws on overflow" {
shouldThrow<ArithmeticException> {
Calculator.add(Int.MAX_VALUE, 1)
}
}
})FunSpec (most familiar to JUnit / pytest users):
class CalculatorTest : FunSpec({
test("adds two numbers") {
Calculator.add(1, 2) shouldBe 3
}
context("overflow handling") {
test("throws on max + 1") {
shouldThrow<ArithmeticException> { Calculator.add(Int.MAX_VALUE, 1) }
}
}
})BehaviorSpec (Given/When/Then BDD): given("…") { \when`("…") {
then("…") { … } } }`.
Per kotest.io/docs/assertions/matchers.html:
| Matcher | Use |
|---|---|
value shouldBe expected / shouldNotBe | Equality |
value.shouldBeNull() / shouldNotBeNull() | Null check |
string.shouldContain("substring") / shouldStartWith / shouldEndWith | String |
string.shouldMatch(regex) | Regex |
list.shouldHaveSize(3) / shouldContainExactly(...) / shouldContainAll(...) | Collection |
map.shouldContainKey("key") / shouldContainValue("v") | Map |
value.shouldBeInstanceOf<MyClass>() | Type |
result.shouldBeSuccess() / shouldBeFailure() | Kotlin Result |
shouldThrow<E> { ... } | Exception |
"addition is commutative" {
checkAll<Int, Int> { a, b ->
a + b shouldBe b + a
}
}
"concatenation length" {
checkAll(Arb.string(), Arb.string()) { a, b ->
(a + b).length shouldBe a.length + b.length
}
}For deeper property-based work see the qa-property-based plugin
(jqwik-testing for the JVM). Don't run Kotest property-based and jqwik in
the same project - pick one.
Test bodies are suspend functions - runTest etc. from
kotlinx-coroutines-test work directly:
"fetches user data" {
val user = fetchUserAsync(1) // suspend function
user.id shouldBe 1
}Data-driven rows via withData - each row reports as a separate test:
context("addition") {
withData(
Triple(1, 2, 3),
Triple(0, 0, 0),
Triple(-1, 1, 0),
) { (a, b, expected) ->
(a + b) shouldBe expected
}
}Per kotest.io/docs/framework/isolation-mode.html:
| Mode | Behavior |
|---|---|
SingleInstance | One spec instance for all tests (default; fastest) |
InstancePerTest | Fresh spec instance per test (incl. nested contexts) |
InstancePerLeaf | Fresh spec instance per leaf-test only |
Set per-spec (isolationMode = IsolationMode.InstancePerTest inside the
spec body) or globally via AbstractProjectConfig. Use a fresh-instance
mode whenever specs carry mutable state.
Kotest's runner is kotest-runner-junit5, so CI is identical to JUnit 5:
./gradlew test jacocoTestReport.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Mix multiple spec styles in one project | Reader confusion | Pick one |
| Default isolation + shared mutable state | Tests interfere | InstancePerTest |
assertEquals(a, b) (JUnit style) in Kotest specs | Mixes paradigms | a shouldBe b |