29、Kotlin测试:单元测试框架、JUnit与Kotlin Test、MockK库、协程测试

说实话,很多Kotlin开发者写代码时很爽,一到测试就头疼。我早期带团队时也踩过这个坑——代码写得飞起,测试覆盖率惨不忍睹。后来我慢慢意识到,测试不是负担,而是代码质量的护城河。今天咱们就聊聊Kotlin生态里那些好用的测试工具。

单元测试框架:JUnit 5 与 Kotlin Test

先说说最基础的。JUnit 5 在 Java 世界里是标配,到了 Kotlin 这边依然能打。不过我个人更推荐 Kotlin Test,它跟 Kotlin 的语法糖配合得更丝滑。

举个例子,JUnit 5 里你写个测试类得这么来:

class CalculatorTest {
    private val calculator = Calculator()

    @Test
    fun `加法测试`() {
        assertEquals(4, calculator.add(2, 2))
    }
}

用 Kotlin Test 的话,写法更自然:

class CalculatorTest : StringSpec({
    "加法测试" {
        calculator.add(2, 2) shouldBe 4
    }
})

看到区别了吗?shouldBe 这种中缀表达式读起来就像在说人话。我在项目中经常用 StringSpecBehaviorSpec,因为测试用例的描述本身就是文档。

💡 我的习惯:测试类名用 `*Test` 结尾,方法名用反引号包裹中文描述。这样测试报告出来,非技术人员也能看懂。

MockK 库:Kotlin 专属的 Mock 利器

Mockito 在 Java 里很流行,但到了 Kotlin 就有点水土不服——final 类、扩展函数、协程……Mockito 处理起来很别扭。这时候 MockK 就派上用场了。

MockK 是专门为 Kotlin 设计的 Mock 框架。我当年从 Mockito 切换到 MockK 时,最大的感受就是:终于不用写那么多样板代码了

看个例子:

class UserServiceTest : StringSpec({
    val userRepo = mockk<UserRepository>()
    val userService = UserService(userRepo)

    "获取用户信息" {
        every { userRepo.findById(1) } returns User(1, "张三")

        val result = userService.getUser(1)

        result.name shouldBe "张三"
        verify { userRepo.findById(1) }
    }
})

这里有几个关键点:

  • mockk() 创建 Mock 对象
  • every { ... } returns ... 定义行为
  • verify { ... } 验证调用
⚠️ 避坑指南:我曾经在项目中用 `relaxed = true` 偷懒,结果某个方法返回了默认值,导致线上 bug 没被测试出来。建议:明确 mock 的行为,不要依赖 relaxed mock

协程测试:runTest 与 TestScope

协程测试是 Kotlin 测试里最让人头疼的部分。我记得第一次写协程测试时,死活跑不过,后来才发现是 测试框架没感知到协程的调度

Kotlin 官方提供了 kotlinx-coroutines-test 库,核心就是 runTestTestScope

看个实际例子:

class CoroutineServiceTest : StringSpec({
    "异步加载数据" {
        val service = CoroutineService()

        runTest {
            val result = service.loadData()
            result shouldBe "数据加载完成"
        }
    }
})

runTest 会自动处理协程的调度,让测试变成同步执行。但要注意,如果你的代码里有 delay()runTest 会跳过等待时间,直接执行后续代码。

再复杂一点的情况,比如测试超时或并发:

runTest {
    withTimeout(1000) {
        val result = withContext(Dispatchers.IO) {
            // 模拟耗时操作
            delay(500)
            "完成"
        }
        result shouldBe "完成"
    }
}
🔑 核心要点:协程测试的关键是 控制时间。`runTest` 里的 `delay` 会被虚拟时间替代,所以测试跑得飞快。但如果你用了 `Dispatchers.IO` 或 `Dispatchers.Default`,记得用 `StandardTestDispatcher` 替换,否则可能出问题。

知识体系总览

下面这张图总结了本章的核心内容,方便你快速回顾:

Kotlin 测试体系 JUnit 5 @Test, assertEquals Kotlin Test StringSpec, shouldBe MockK mockk, every, verify 协程测试 runTest, TestScope, StandardTestDispatcher 测试策略建议 单元测试用 Kotlin Test + MockK 协程测试用 runTest + TestScope

实战建议:测试金字塔

说了这么多工具,最后聊聊策略。我一般遵循 测试金字塔 原则:

层级 比例 工具 说明
单元测试 70% Kotlin Test + MockK 测试单个类或函数,速度快
集成测试 20% JUnit 5 + TestContainers 测试数据库、网络等外部依赖
端到端测试 10% 自定义框架 模拟真实用户场景

说白了,单元测试是性价比最高的。你花 20% 的时间写单元测试,能发现 80% 的 bug。我见过太多团队一上来就搞端到端测试,结果维护成本高、跑得慢,最后测试用例全废了。

💡 我的经验:写测试时,先想清楚「这个函数到底要保证什么」。不要为了覆盖率而写测试,要为了 验证行为 而写。好的测试,读起来就像一份可执行的文档。

好了,关于 Kotlin 测试的核心内容就这些。记住:测试不是可选项,是必需品。从今天开始,给你的每个函数都配上测试吧。

公众号:蓝海资料掘金营,微信deep3321