Agent skill
kotlin-testing
Kotlin testing with JUnit 5, Kotest, and coroutine dispatchers.
Install this agent skill to your Project
npx add-skill https://github.com/notque/claude-code-toolkit/tree/main/skills/kotlin-testing
SKILL.md
Kotlin Testing Patterns
JUnit 5 Fundamentals
Use @Test for simple test cases. Prefer @DisplayName for readable test names.
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.DisplayName
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class UserServiceTest {
@Test
@DisplayName("should return user by ID when user exists")
fun findUserById() {
val service = UserService(FakeUserRepository())
val user = service.findById(1L)
assertEquals("Alice", user.name)
}
@Test
fun `should throw when user not found`() {
val service = UserService(FakeUserRepository())
assertFailsWith<UserNotFoundException> {
service.findById(999L)
}
}
}
Parameterized Tests
Use @ParameterizedTest with @MethodSource for complex inputs or @CsvSource for simple value pairs.
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.params.provider.Arguments
import java.util.stream.Stream
class ValidatorTest {
@ParameterizedTest
@CsvSource(
"alice@example.com, true",
"not-an-email, false",
"'', false"
)
fun `should validate email addresses`(input: String, expected: Boolean) {
assertEquals(expected, EmailValidator.isValid(input))
}
@ParameterizedTest
@MethodSource("provideUserInputs")
fun `should reject invalid users`(input: UserInput, expectedError: String) {
val result = UserValidator.validate(input)
assertTrue(result.isFailure)
assertEquals(expectedError, result.exceptionOrNull()?.message)
}
companion object {
@JvmStatic
fun provideUserInputs(): Stream<Arguments> = Stream.of(
Arguments.of(UserInput(name = "", age = 25), "Name must not be blank"),
Arguments.of(UserInput(name = "Bob", age = -1), "Age must be positive"),
)
}
}
Table-Driven Tests
Kotlin's data classes and list literals make table-driven tests natural without frameworks.
@Test
fun `should parse duration strings correctly`() {
data class Case(val input: String, val expectedMs: Long, val label: String)
val cases = listOf(
Case("100ms", 100, "milliseconds"),
Case("5s", 5000, "seconds"),
Case("2m", 120_000, "minutes"),
Case("1h", 3_600_000, "hours"),
)
cases.forEach { (input, expectedMs, label) ->
val result = DurationParser.parse(input)
assertEquals(expectedMs, result, "Failed for $label: input=$input")
}
}
Kotest Styles
Kotest offers multiple spec styles. Pick one per project for consistency.
import io.kotest.core.spec.style.FunSpec
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.assertions.throwables.shouldThrow
// FunSpec — closest to JUnit, good default choice
class CalculatorFunSpec : FunSpec({
test("addition of two numbers") {
Calculator.add(2, 3) shouldBe 5
}
context("division") {
test("divides evenly") {
Calculator.divide(10, 2) shouldBe 5
}
test("throws on divide by zero") {
shouldThrow<ArithmeticException> {
Calculator.divide(1, 0)
}
}
}
})
// BehaviorSpec — Given/When/Then for acceptance-style tests
class OrderBehaviorSpec : BehaviorSpec({
Given("a cart with two items") {
val cart = Cart().apply {
add(Item("Widget", 9.99))
add(Item("Gadget", 19.99))
}
When("checkout is completed") {
val order = cart.checkout()
Then("order total reflects both items") {
order.total shouldBe 29.98
}
Then("order contains two line items") {
order.items.size shouldBe 2
}
}
}
})
// StringSpec — minimal boilerplate for simple tests
class StringSpecExample : StringSpec({
"length of hello should be 5" {
"hello".length shouldBe 5
}
"trimmed string should not contain leading spaces" {
" padded ".trim() shouldBe "padded"
}
})
Kotest Matchers
Kotest provides expressive matchers beyond shouldBe.
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.string.shouldStartWith
import io.kotest.matchers.nulls.shouldNotBeNull
val names = listOf("Alice", "Bob", "Charlie")
names shouldHaveSize 3
names shouldContainExactly listOf("Alice", "Bob", "Charlie")
val greeting: String? = getGreeting()
greeting.shouldNotBeNull()
greeting shouldStartWith "Hello"
Coroutine Testing
Use runTest from kotlinx-coroutines-test to test suspending functions. runTest auto-advances virtual time.
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
class NotificationServiceTest {
private val testDispatcher = StandardTestDispatcher()
@Test
fun `should send notification after delay`() = runTest(testDispatcher) {
val service = NotificationService(dispatcher = testDispatcher)
service.scheduleNotification("Hello", delayMs = 5000)
advanceUntilIdle()
assertEquals(1, service.sentCount)
}
@Test
fun `should cancel pending notifications`() = runTest(testDispatcher) {
val service = NotificationService(dispatcher = testDispatcher)
val job = service.scheduleNotification("Hello", delayMs = 5000)
job.cancel()
advanceUntilIdle()
assertEquals(0, service.sentCount)
}
}
MockK
MockK is the idiomatic Kotlin mocking library. Use every {} for stubs, verify {} for assertions, and coEvery {} / coVerify {} for suspending functions.
import io.mockk.mockk
import io.mockk.every
import io.mockk.coEvery
import io.mockk.verify
import io.mockk.coVerify
import io.mockk.slot
class OrderServiceTest {
private val repo = mockk<OrderRepository>()
private val notifier = mockk<Notifier>(relaxed = true)
private val service = OrderService(repo, notifier)
@Test
fun `should save order and notify`() {
val orderSlot = slot<Order>()
every { repo.save(capture(orderSlot)) } returns Order(id = 42)
service.placeOrder(items = listOf("Widget"))
assertEquals(42, orderSlot.captured.id)
verify(exactly = 1) { notifier.send(any()) }
}
@Test
fun `should fetch order from remote API`() = runTest {
coEvery { repo.fetchRemote(1L) } returns Order(id = 1, status = "shipped")
val order = service.getOrder(1L)
assertEquals("shipped", order.status)
coVerify { repo.fetchRemote(1L) }
}
}
Assertion Libraries Summary
| Library | Syntax Style | Best For |
|---|---|---|
kotlin.test |
assertEquals(expected, actual) |
JUnit 5 projects, zero extra deps |
| Kotest matchers | actual shouldBe expected |
Readable assertions, rich matcher library |
| AssertJ (via assertk) | assertThat(actual).isEqualTo(expected) |
Java interop, fluent chains |
Key Principles
- One assertion concept per test -- a test should verify one behavior, though it may need multiple assertions to do so.
- Use
runTestfor all coroutine tests -- never userunBlockingin tests;runTesthandles virtual time and uncaught exceptions. - Prefer fakes over mocks -- mocks couple tests to implementation; fakes (manual implementations) couple tests to contracts.
- Name tests as behavior specifications -- backtick names like
`should return empty list when no results`read better in reports. - Inject dispatchers -- never hardcode
Dispatchers.IOin production code; accept aCoroutineDispatcherparameter so tests can supplyTestDispatcher.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
voice-writer
Unified voice content generation pipeline with mandatory validation and joy-check. 9-phase pipeline: LOAD, GROUND, GENERATE, VALIDATE, REFINE, JOY-CHECK, OUTPUT, CLEANUP. Use when writing articles, blog posts, or any content that uses a voice profile. Use for "write article", "blog post", "write in voice", "generate content", "draft article", "write about".
image-auditor
Non-destructive image validation for accessibility and health.
video-editing
Video editing pipeline: cut footage, assemble clips via FFmpeg and Remotion.
comment-quality
Review and fix temporal references in code comments.
e2e-testing
Playwright-based end-to-end testing workflow.
anti-ai-editor
Remove AI-sounding patterns from content.
Didn't find tool you were looking for?