From kotlin
Write clear, deterministic Kotlin tests that prove one observable behavior with the smallest correct scope. Use this skill when the user asks to "test Kotlin code", "write a coroutine test", "mock a Kotlin dependency", "structure Kotlin tests", or needs guidance on practical Kotlin testing patterns.
npx claudepluginhub ririnto/sinon --plugin kotlinThis skill uses the workspace's default tool permissions.
Write clear, deterministic Kotlin tests by proving one observable behavior with the smallest scope that works.
agents/openai.yamlreferences/coroutine-test-determinism.mdreferences/eventual-consistency-and-awaitility.mdreferences/flow-testing.mdreferences/gradle-dependencies-and-config.mdreferences/junit5-structure-and-timeouts.mdreferences/kotest-style-and-exact-exceptions.mdreferences/mocking-boundaries-and-mockk.mdreferences/turbine-flow-testing.mdMandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Write clear, deterministic Kotlin tests by proving one observable behavior with the smallest scope that works.
Minimum Kotlin version: 1.9 -- examples use kotlin.test baseline assertions, kotlinx.coroutines.test (1.7+), JUnit 5 Jupiter APIs, MockK 1.14+, Kotest 6.x, and Turbine 1.2+. All library versions are managed through the project's dependency catalog; pin versions when adopting features from specific releases. This skill covers JVM testing only -- for multiplatform targets, adapt assertions to kotlin-test-js or kotlin-test-native. Keep the common path centered on kotlin.test, runTest for suspend code, bounded Flow collection, and direct exception assertions, then open a blocker reference only when virtual time, replay semantics, mocking-library details, or JUnit 5 structure features become the real problem.
verbCondition or subjectVerb describing the observable behavior (e.g., returnsCachedProfile, emitsLoadingThenData, rejectsInvalidInput).kotlin.test annotations and baseline assertions as the default surface.runTest when coroutine semantics actually matter.first(), single(), or take(n).toList().assertFailsWith<T>() when exception type is part of the contract.kotlin.test assertions and plain synchronous structure.runTest only when the code under test uses suspend, delay, cancellation, or Flow collection, and make sure kotlinx-coroutines-test is available in test scope.kotlin.testUse @Test and baseline assertions first. Prefer the multi-assertion form that checks several properties in one test body.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertNull
import kotlin.test.assertContentEquals
import kotlin.test.assertContains
class ProfileServiceTest {
@Test
fun returnsCachedProfile() {
val result = service.loadProfile("user-1")
assertEquals(Profile("user-1"), result)
assertTrue(result.isActive)
assertNull(result.error)
}
@Test
fun returnsTagList() {
val tags = service.loadTags("user-1")
assertContentEquals(listOf("admin", "editor"), tags)
assertContains(tags, "admin")
}
}
These are the common kotlin.test assertions you will reach for most often:
| Assertion | When to use it |
|---|---|
assertEquals(expected, actual) | value equality is the contract |
assertTrue(condition) / assertFalse(condition) | boolean predicate is the contract |
assertNull(value) / assertNotNull(value) | nullability is part of the contract |
assertContentEquals(expected, actual) | comparing lists, arrays, or sequences by element |
assertContains(collection, element) / assertContains(charSequence, value) | membership check on collections or strings |
assertNotEquals(illegal, actual) | proving a value is not something specific |
assertSame(expected, actual) | referential identity (not equality) matters |
assertFailsWith<T> { ... } | thrown type and message/properties are the contract |
fail(reason) | mark an unreachable branch as a test failure |
Layer JUnit 5 annotations such as @DisplayName, @BeforeEach, or @ParameterizedTest only when the suite already uses Jupiter features.
Import rule: When using any JUnit 5 feature (@Nested, @ParameterizedTest, @DisplayName, @TempDir, etc.), import @Test from org.junit.jupiter.api.Test. When using only kotlin.test features, import @Test from kotlin.test.Test. Never mix both imports in the same file -- the compiler cannot resolve which @Test you mean.
runTest for suspend coderunTest is the ordinary path for coroutine-aware tests. It skips delays and surfaces uncaught child-coroutine failures.
When code under test uses withTimeout, a timed-out delay inside runTest throws TimeoutCancellationException (a subclass of CancellationException). Since runTest handles CancellationException at scope level, time out assertions work naturally -- the test body completes and you assert on the fallback result:
@Test
fun returnsFallbackOnTimeout() = runTest {
advanceTimeBy(5_000)
val result = service.loadWithTimeout(OrderId("1"))
assertEquals(Fallback, result)
}
Do not wrap runTest bodies in try/catch for CancellationException or TimeoutCancellationException -- runTest manages cancellation lifecycle automatically.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
class OrderSummaryServiceTest {
@Test
fun loadsOrderSummary() = runTest {
val result = service.loadSummary(OrderId("o-1"))
assertEquals(OrderSummary("o-1"), result)
}
}
Use first(), single(), or take(n).toList() to prove a finite contract and finish the test.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
class UiStateRepositoryTest {
@Test
fun emitsLoadingThenData() = runTest {
val items = repository.observe().take(2).toList()
assertTrue(items.size == 2)
assertEquals(UiState.Loading, items[0])
assertEquals(UiState.Data, items[1])
}
}
Use assertFailsWith<T>() when the thrown type is part of the contract. It returns the exception, so message or property checks can stay explicit.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class RetryPolicyTest {
private val service = RetryPolicyService()
@Test
fun rejectsInvalidRetryBudget() {
val error = assertFailsWith<IllegalArgumentException> {
service.configure(-1)
}
assertEquals("retry budget must be non-negative", error.message)
}
}
If the path is still unclear, write one synchronous behavior test first, then add runTest or bounded Flow collection only if the production contract requires it.
Check these pass/fail conditions before you stop:
runTest instead of real sleeps| Anti-pattern | Why it fails | Correct move |
|---|---|---|
| asserting internal call order instead of behavior | the test couples to implementation noise | assert the visible contract first |
| using real delays in coroutine tests | the suite becomes slow and flaky | use runTest and scheduler control |
| collecting a Flow forever | the test never reaches a bounded assertion | use first(), single(), or take(n).toList() |
| over-mocking simple values or pure helpers | fixtures become harder to read than the code under test | keep simple values real |
| reaching for framework-specific helpers before a plain test works | the test shape becomes heavier than the behavior | start with kotlin.test and grow only when needed |
using assertEquals on lists when element order is unstable | structural comparison fails on reorderings | use assertContains or sort before assertEquals |
Return:
runTest, or uses bounded Flow collectionOpen only the reference that matches the remaining blocker.
| Open when... | Read... |
|---|---|
| step-by-step Flow inspection, cancellation verification, or error-terminal states are the blocker | ./references/turbine-flow-testing.md |
| setting up test dependencies, Gradle configuration, or choosing libraries is the blocker | ./references/gradle-dependencies-and-config.md |
| delay control, scheduler advancement, or dispatcher injection is the blocker | ./references/coroutine-test-determinism.md |
| Flow replay semantics or bounded collection shape is the blocker | ./references/flow-testing.md |
| JUnit 5 nested structure, grouped assertions, or timeout variants are the blocker | ./references/junit5-structure-and-timeouts.md |
| mocking boundaries or MockK collaboration checks are the blocker | ./references/mocking-boundaries-and-mockk.md |
| Kotest style, soft assertions, or exact exception checks are the blocker | ./references/kotest-style-and-exact-exceptions.md |
| eventual consistency requires Awaitility rather than scheduler control or bounded collection | ./references/eventual-consistency-and-awaitility.md |
Use this skill for Kotlin JVM unit and integration test shape, coroutine-aware test execution, bounded Flow assertions, and practical mocking-boundary choices. This skill covers JVM testing with JUnit 5, MockK (JVM), Kotest, and Turbine. For multiplatform Kotlin testing (kotlin-test-js, kotlin-test-native), adapt assertions to the target platform's available surface.
Do not use this skill as the primary source for coroutine API design (use kotlin-coroutines-flows), general Kotlin language refactors (use kotlin-language-patterns), or framework-heavy application-context testing such as Spring @SpringBootTest.