From qa-unit-tests-jvm
Configures and runs Kotest - Kotlin-native test framework with multiple specification styles (StringSpec, FunSpec, BehaviorSpec, DescribeSpec, ShouldSpec, FreeSpec, FeatureSpec, ExpectSpec, AnnotationSpec); rich matcher library; built-in property-based testing (alternative to jqwik); coroutines support; data-driven testing; isolation modes per-spec or per-test; integrates with Gradle JVM test task. Use when working with Kotlin and wanting Kotlin-idiomatic DSL over JUnit 5's annotation-driven approach.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-unit-tests-jvm:kotest-testsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Per [kotest.io/docs][kt-docs]:
Per kotest.io/docs:
Kotest is the Kotlin-native test framework. Differentiated from JUnit 5 (which works fine with Kotlin too) by:
shouldBe, shouldContain, shouldThrow, etc.jqwik-testingrunTest/runBlocking integrated cleanlyFor multi-language JVM projects, JUnit 5 is more universal. For Kotlin-only or Kotlin-primary, Kotest's DSL is more ergonomic.
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 Kotest supports 8+ styles. Common picks:
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):
class UserServiceTest : BehaviorSpec({
given("a registered user") {
val user = User("[email protected]")
`when`("they update their email") {
user.updateEmail("[email protected]")
then("the email is updated") {
user.email shouldBe "[email protected]"
}
}
}
})
Pick one style per project + stick with it.
Per kotest.io/docs/assertions/matchers.html:
Core matchers:
| Matcher | Use |
|---|---|
value shouldBe expected | Equality |
value shouldNotBe expected | Inequality |
value should be(expected) | Same; alternate syntax |
value.shouldBeNull() / shouldNotBeNull() | Null check |
string.shouldContain("substring") | String membership |
string.shouldStartWith("prefix") / shouldEndWith("suffix") | String pos |
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 |
Built-in (no separate library):
class PropertyTest : StringSpec({
"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 jqwik-testing
or the dedicated qa-property-based plugin.
class AsyncTest : StringSpec({
"fetches user data" {
val user = fetchUserAsync(1) // suspend function
user.id shouldBe 1
}
})
Test bodies are suspend functions; runTest etc. wrappers from
kotlinx-coroutines-test work directly.
class DataDrivenTest : FunSpec({
context("addition") {
withData(
Triple(1, 2, 3),
Triple(0, 0, 0),
Triple(-1, 1, 0),
) { (a, b, expected) ->
(a + b) shouldBe expected
}
}
})
Each row reports as a separate test - failures don't stop subsequent rows.
Per kotest.io/docs/framework/isolation-mode.html:
Four modes (default SingleInstance):
| 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:
class StatefulTest : StringSpec({
isolationMode = IsolationMode.InstancePerTest
// ...
})
Or globally via AbstractProjectConfig.
Same as JUnit 5 (Kotest's runner is kotest-runner-junit5):
- run: ./gradlew test jacocoTestReport
JaCoCo coverage works identically.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Mix multiple spec styles in one project | Reader confusion | Pick one (Step 2) |
| Default isolation + shared mutable state | Tests interfere | InstancePerTest mode (Step 7) |
| Use Kotest property-based + jqwik in same project | Two PB libraries | Pick one |
assertEquals(a, b) (JUnit style) | Mixes paradigms | Use a shouldBe b (Step 3) |
shouldContainExactly vs
shouldContainAll).junit5-tests,
spock-tests,
testng-tests,
scalatest - sister toolsjqwik-testing - JVM property-basedtest-code-conventionsnpx claudepluginhub testland/qa --plugin qa-unit-tests-jvmProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.