Kotlin: Kotlin测试JUnit5与MockK详解
最后更新:2026-08-26
Bob 的信条:没有测试的代码就是未验证的假设。JUnit 5 + MockK 是 Kotlin 测试的黄金组合——MockK 为 Kotlin 原生设计(支持协程 mock),JUnit 5 提供现代测试框架。
1. 你将学到
- JUnit 5 + Kotlin:、、嵌套测试
- MockK:Kotlin 原生 Mock 框架
- 协程测试: / /
- 测试生命周期: /
- Charlie 实战:OrderProcessorTest 全覆盖
2. 一个测试负责人的真实故事
(1) 痛点:Mockito 与 Kotlin 不兼容
Bob 用 Mockito mock Kotlin 类时频繁遇到 Final class 错误(Kotlin 类默认 final),需要配置 扩展。协程 函数更是完全无法 mock。
(2) MockK 的解法
KOTLIN
// Mockito: problems with final classes and suspend functions
when(repo.findById("ORD-001")).thenReturn(order) // Fails on final class!
// MockK: Kotlin native, supports everything
every { repo.findById("ORD-001") } returns order // Works on any class!
coEvery { repo.fetchOrderAsync("ORD-001") } returns order // suspend functions!
MockK 为 Kotlin 原生设计——无需额外配置,完美支持 final 类、扩展函数和协程。
3. JUnit 5 基础
(1) 依赖配置
KOTLIN
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}
tasks.test {
useJUnitPlatform()
}
(2) 基本测试
KOTLIN
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class OrderTest {
@Test
fun `should create order with correct id`() {
val order = Order("ORD-001", 299.99, "PENDING")
assertEquals("ORD-001", order.id)
assertEquals(299.99, order.total)
assertEquals("PENDING", order.status)
}
@Test
fun `should throw on negative total`() {
assertThrows(IllegalArgumentException::class.java) {
Order("ORD-001", -50.0, "PENDING")
}
}
}
(3) 参数化测试
KOTLIN
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
class OrderPriorityTest {
@ParameterizedTest
@CsvSource(
"100.0, LOW",
"1500.0, MEDIUM",
"15000.0, HIGH"
)
fun `should determine priority based on total`(total: Double, expected: String) {
val priority = when {
total > 10_000 -> "HIGH"
total > 1_000 -> "MEDIUM"
else -> "LOW"
}
assertEquals(expected, priority)
}
}
(4) 嵌套测试
KOTLIN
class OrderProcessorTest {
@Nested
inner class OrderCreation {
@Test
fun `should create pending order`() { ... }
@Test
fun `should reject negative total`() { ... }
}
@Nested
inner class OrderProcessing {
@Test
fun `should confirm pending order`() { ... }
@Test
fun `should reject confirming shipped order`() { ... }
}
}
4. MockK 基础
(1) Mock 创建与存根
KOTLIN
import io.mockk.*
class OrderServiceTest {
// Create mock
val repo = mockk`<OrderRepository>`()
val service = OrderService(repo)
@Test
fun `should find order by id`() {
// Stub: define behavior
every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING")
every { repo.findById(any()) } returns null
// Act
val order = service.findOrder("ORD-001")
// Assert
assertEquals(299.99, order?.total)
// Verify: check interaction
verify(exactly = 1) { repo.findById("ORD-001") }
}
}
(2) MockK 常用 API
| API | 用途 | 示例 |
|---|---|---|
| 存根返回值 | ||
| 存根抛异常 | ||
| 动态计算 | ||
| 验证调用 | ||
| 验证调用顺序 | ||
| 确认无未验证调用 | ||
| 协程存根 | ||
| 协程验证 |
(3) MockK vs Mockito
| 维度 | Mockito | MockK |
|---|---|---|
| Kotlin final 类 | 需额外配置 | 原生支持 |
| 协程 mock | 不支持 | / |
| 扩展函数 mock | 不支持 | |
| 对象 mock | 困难 | |
| 语法 | Java 风格 | Kotlin DSL 风格 |
5. 协程测试
(1) runTest
KOTLIN
import kotlinx.coroutines.test.runTest
class OrderServiceTest {
val repo = mockk`<OrderRepository>`()
val service = OrderService(repo)
@Test
fun `should fetch order asynchronously`() = runTest {
// Given
coEvery { repo.fetchOrder("ORD-001") } returns Order("ORD-001", 299.99, "PENDING")
// When
val order = service.fetchOrderAsync("ORD-001")
// Then
assertEquals(299.99, order.total)
coVerify { repo.fetchOrder("ORD-001") }
}
}
(2) 协程测试工具
| 工具 | 用途 |
|---|---|
| 协程测试入口(替代 runBlocking) | |
| 协程函数存根 | |
| 协程函数验证 | |
| 控制协程调度(虚拟时间) | |
| 推进所有待处理协程 |
6. 测试生命周期
KOTLIN
class OrderServiceTest {
private lateinit var repo: OrderRepository
private lateinit var service: OrderService
@BeforeEach
fun setup() {
repo = mockk()
service = OrderService(repo)
}
@AfterEach
fun cleanup() {
unmockkAll() // Clear all mocks
}
@Test
fun `should process order`() { ... }
}
(1) 生命周期注解
| 注解 | 执行时机 | 用途 |
|---|---|---|
| 每个测试前 | 初始化 mock 和测试对象 | |
| 每个测试后 | 清理资源 | |
| 所有测试前(需伴生对象) | 昂贵的一次性初始化 | |
| 所有测试后 | 全局清理 |
7. 测试金字塔与流程
flowchart TD
A[Unit Tests<br/>MockK + JUnit5<br/>Fast, isolated] --> B[Integration Tests<br/>Spring Boot Test<br/>Real DB/HTTP]
B --> C[E2E Tests<br/>TestContainers<br/>Full stack]
A --> D[runTest for coroutines]
A --> E[MockK for dependencies]
D --> F[Virtual time control]
8. 完整示例:OrderProcessorTest
KOTLIN
// ============================================
// OrderProcessor - Test Suite
// Feature: Full test coverage with JUnit5 + MockK
// ============================================
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
import io.mockk.*
import kotlinx.coroutines.test.runTest
// Domain classes
data class Order(val id: String, val total: Double, var status: String, val customerId: String)
interface OrderRepository {
fun findById(id: String): Order?
fun save(order: Order)
suspend fun fetchOrderAsync(id: String): Order
}
class OrderService(private val repo: OrderRepository) {
fun findOrder(id: String): Order = repo.findById(id) ?: throw NoSuchElementException("Order $id not found")
fun processOrder(id: String): Order {
val order = findOrder(id)
if (order.status != "PENDING") throw IllegalStateException("Order $id is not pending")
order.status = "CONFIRMED"
repo.save(order)
return order
}
suspend fun fetchOrderAsync(id: String): Order = repo.fetchOrderAsync(id)
fun calculatePriority(order: Order): String = when {
order.total > 10_000 -> "HIGH"
order.total > 1_000 -> "MEDIUM"
else -> "LOW"
}
}
class OrderServiceTest {
private lateinit var repo: OrderRepository
private lateinit var service: OrderService
@BeforeEach
fun setup() {
repo = mockk(relaxed = true) // Relaxed: returns default values for unstubbed methods
service = OrderService(repo)
}
@AfterEach
fun cleanup() {
unmockkAll()
}
@Nested
@DisplayName("Order Retrieval")
inner class OrderRetrieval {
@Test
fun `should find existing order`() {
// Given
every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")
// When
val order = service.findOrder("ORD-001")
// Then
assertEquals("ORD-001", order.id)
assertEquals(299.99, order.total)
}
@Test
fun `should throw when order not found`() {
every { repo.findById(any()) } returns null
assertThrows(NoSuchElementException::class.java) {
service.findOrder("ORD-999")
}
}
}
@Nested
@DisplayName("Order Processing")
inner class OrderProcessing {
@Test
fun `should confirm pending order`() {
every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")
every { repo.save(any()) } just Runs
val processed = service.processOrder("ORD-001")
assertEquals("CONFIRMED", processed.status)
verify { repo.save(match { it.status == "CONFIRMED" }) }
}
@Test
fun `should reject non-pending order`() {
every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "SHIPPED", "CUST-001")
assertThrows(IllegalStateException::class.java) {
service.processOrder("ORD-001")
}
verify(exactly = 0) { repo.save(any()) }
}
}
@Nested
@DisplayName("Priority Calculation")
inner class PriorityCalculation {
@Test
fun `should assign HIGH priority for orders over 10000`() {
val order = Order("ORD-001", 15_000.00, "PENDING", "CUST-001")
assertEquals("HIGH", service.calculatePriority(order))
}
@Test
fun `should assign MEDIUM priority for orders between 1000 and 10000`() {
val order = Order("ORD-001", 2_500.00, "PENDING", "CUST-001")
assertEquals("MEDIUM", service.calculatePriority(order))
}
@Test
fun `should assign LOW priority for orders under 1000`() {
val order = Order("ORD-001", 299.99, "PENDING", "CUST-001")
assertEquals("LOW", service.calculatePriority(order))
}
}
@Nested
@DisplayName("Async Operations")
inner class AsyncOperations {
@Test
fun `should fetch order asynchronously`() = runTest {
coEvery { repo.fetchOrderAsync("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")
val order = service.fetchOrderAsync("ORD-001")
assertEquals("ORD-001", order.id)
coVerify { repo.fetchOrderAsync("ORD-001") }
}
}
}
输出:所有测试通过 ✅
❓ 常见问题
Q MockK 的 relaxed mock 和普通 mock 有什么区别?
A Relaxed mock()对未存根的方法返回默认值(0/null/false),普通 mock 对未存根方法抛 MockKException。推荐用普通 mock,避免隐藏未设置的行为。
Q runTest 和 runBlocking 有什么区别?
A 使用虚拟时间(跳过 delay),测试秒级完成; 使用真实时间,delay 多久就等多久。协程测试必须用 。
Q 如何 mock 伴生对象?
A 用 mock 整个伴生对象,。测试后用 恢复。
Q 如何 mock 扩展函数?
A 用 mock 特定扩展函数。但尽量避免——扩展函数通常可以重构为普通函数以提高可测试性。
Q 测试应该覆盖多少百分比?
A 目标 80% 行覆盖,但覆盖率不等于质量。关键业务逻辑(支付、库存、状态机)应 100% 覆盖,简单 data class 可以不测试。
Q 集成测试和单元测试怎么区分?
A 单元测试 mock 所有依赖(测试单个类),集成测试使用真实依赖(数据库、HTTP)。单元测试快且稳定,集成测试慢但更真实。
📖 小节
- JUnit 5 + MockK 是 Kotlin 测试黄金组合
- MockK 原生支持 Kotlin:final 类、协程、扩展函数、对象
- 存根, 验证调用
- 用于协程测试,使用虚拟时间无需等待 delay
- / 专门用于 suspend 函数 mock 和验证
- 嵌套测试 组织相关测试,参数化测试 减少重复
📝 作业
- 基础题(难度⭐):为 编写 JUnit 5 测试,验证 和 行为。提示:
- 进阶题(难度⭐⭐):用 MockK mock ,测试 的正常和异常路径。提示:
- 挑战题(难度⭐⭐⭐):用 + 测试异步订单获取函数,验证协程调用的时序。提示: