JVM unit testing (Java / Kotlin / Scala / Groovy) with JUnit 5 (Jupiter) as the primary framework - annotations (`@Test` / `@ParameterizedTest` / source providers), lifecycle hooks (`@BeforeAll` / `@BeforeEach`), extension model (`@ExtendWith` + Mockito/Spring), display names, conditional execution, parallel-execution config, JaCoCo coverage, and Maven Surefire / Gradle CI. Includes a per-language framework decision table (Java → JUnit 5, Kotlin → Kotest, Groovy → Spock, Scala → ScalaTest, legacy → TestNG; always match an existing build convention) and test-authoring conventions (framework detection from pom.xml / build.gradle / build.sbt, path conventions, no fabricated methods). References cover Kotest spec styles, Spock given/when/then + data tables, TestNG DataProviders + suites, ScalaTest styles + Matchers, and the AssertJ fluent-assertion catalog. Use for any JVM unit-test task: choosing or configuring a framework, writing or parameterizing tests, wiring coverage and CI.
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
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 |