# Kotlin Testing

> When to activate: Kotlin testing, JUnit 5, Kotest, MockK, Testcontainers, coroutine testing, Spring test slices, runTest

- Skill: `mattakushi432/kotlin-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/kotlin-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/kotlin-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/kotlin-testing

---

# Kotlin Testing Patterns

## JUnit 5 + MockK

```kotlin
@ExtendWith(MockKExtension::class)
class UserServiceTest {

    @MockK lateinit var userRepository: UserRepository
    @MockK lateinit var emailService: EmailService
    @InjectMockKs lateinit var userService: UserService

    @Test
    fun `findById returns user when exists`() {
        val user = User(id = 1L, name = "Alice", email = "alice@example.com")
        every { userRepository.findById(1L) } returns Optional.of(user)

        val result = userService.findById(1L)

        assertThat(result).isEqualTo(user.toDto())
        verify(exactly = 1) { userRepository.findById(1L) }
    }

    @Test
    fun `findById throws when not found`() {
        every { userRepository.findById(any()) } returns Optional.empty()

        assertThrows<ResourceNotFoundException> { userService.findById(99L) }
    }

    @Test
    fun `create sends welcome email`() {
        val request = CreateUserRequest("Bob", "bob@example.com")
        every { userRepository.existsByEmail(any()) } returns false
        every { userRepository.save(any()) } answers { firstArg() }
        every { emailService.sendWelcome(any()) } just Runs

        userService.create(request)

        verify { emailService.sendWelcome(match { it.email == "bob@example.com" }) }
    }
}
```

## Kotest

```kotlin
class UserServiceSpec : BehaviorSpec({
    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    Given("a user exists") {
        val user = User(1L, "Alice", "alice@example.com")
        every { repository.findById(1L) } returns Optional.of(user)

        When("findById is called") {
            val result = service.findById(1L)

            Then("returns the user DTO") {
                result shouldBe user.toDto()
            }
        }
    }
})

// Data-driven tests
class EmailValidationSpec : FunSpec({
    forAll(
        row("valid@email.com", true),
        row("invalid-email", false),
        row("", false),
        row("missing@domain", false),
    ) { email, expected ->
        test("$email is ${if (expected) "valid" else "invalid"}") {
            email.isValidEmail() shouldBe expected
        }
    }
})
```

## Coroutine Testing

```kotlin
class FlowViewModelTest {
    @get:Rule val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun `search results update after debounce`() = runTest {
        val fakeRepo = FakeSearchRepository()
        val vm = SearchViewModel(fakeRepo)

        vm.onQueryChange("kotlin")
        advanceTimeBy(400) // past 300ms debounce

        assertEquals(fakeRepo.expectedResults, vm.results.value)
    }

    @Test
    fun `stateflow collects values`() = runTest {
        val flow = MutableStateFlow(0)
        val collected = mutableListOf<Int>()

        val job = launch { flow.collect { collected.add(it) } }
        flow.value = 1
        flow.value = 2
        job.cancel()

        assertThat(collected).containsExactly(0, 1, 2)
    }
}
```

## Testcontainers

```kotlin
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {

    companion object {
        @Container @JvmStatic
        val postgres = PostgreSQLContainer("postgres:16-alpine")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test")

        @DynamicPropertySource @JvmStatic
        fun configureProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.datasource.url", postgres::getJdbcUrl)
            registry.add("spring.datasource.username", postgres::getUsername)
            registry.add("spring.datasource.password", postgres::getPassword)
        }
    }

    @Autowired lateinit var repository: UserRepository

    @Test
    @Transactional
    fun `save and find user`() {
        val user = User(name = "Test", email = "test@example.com")
        val saved = repository.save(user)
        val found = repository.findById(saved.id!!)
        assertThat(found).isPresent.get().extracting("email").isEqualTo("test@example.com")
    }
}
```

## Spring Test Slices

```kotlin
// Only loads web layer
@WebMvcTest(UserController::class)
class UserControllerTest {
    @Autowired lateinit var mvc: MockMvc
    @MockBean lateinit var userService: UserService

    @Test
    fun `GET user returns 200`() {
        every { userService.findById(1L) } returns UserResponse(1L, "Alice")

        mvc.get("/api/v1/users/1")
            .andExpect { status { isOk() } }
            .andExpect { jsonPath("$.name") { value("Alice") } }
    }
}

// Only loads data layer
@DataJpaTest
class OrderRepositoryTest {
    @Autowired lateinit var repository: OrderRepository
    @Autowired lateinit var entityManager: TestEntityManager
    // ...
}
```

## AssertJ Custom Assertions

```kotlin
fun assertThatUser(user: User) = UserAssert(user)

class UserAssert(actual: User) : AbstractAssert<UserAssert, User>(actual, UserAssert::class.java) {
    fun isActive() = apply { check("status") { actual.status == UserStatus.ACTIVE } }
    fun hasEmail(email: String) = apply { check("email") { actual.email == email } }
}

// Usage
assertThatUser(user).isActive().hasEmail("alice@example.com")
```

## Key Rules
- Use `mockk` over Mockito — idiomatic Kotlin, no `any()` casting issues, supports `object` mocking
- Use `runTest` for coroutine tests — it uses `TestCoroutineScheduler` and auto-advances virtual time
- `@Testcontainers` with `@Container` static field — reuse containers across tests with companion object
- Spring slices (`@WebMvcTest`, `@DataJpaTest`) are faster than full `@SpringBootTest` — use them for focused tests
- Prefer `every { } returns` over `whenever { } thenReturn` — cleaner in Kotlin

