多平台测试框架
KMP 项目的测试框架需要支持多平台运行。本文介绍常用的测试框架和最佳实践。
kotlin.test 框架
Kotlin 官方提供的跨平台测试框架,是 KMP 项目的首选。
基础使用
kotlin
// commonTest
import kotlin.test.*
class MathUtilsTest {
@Test
fun testAdd() {
assertEquals(5, add(2, 3))
}
@Test
fun testDivide() {
assertEquals(2.0, divide(6.0, 3.0))
}
@Test
fun testDivideByZero() {
assertFailsWith<ArithmeticException> {
divide(1.0, 0.0)
}
}
}断言方法
| 方法 | 用途 |
|---|---|
assertEquals(expected, actual) | 值相等 |
assertTrue(condition) | 条件为真 |
assertFalse(condition) | 条件为假 |
assertNull(value) | 值为 null |
assertNotNull(value) | 值非 null |
assertFailsWith<T> | 抛出指定异常 |
assertContains(collection, element) | 包含元素 |
测试生命周期
kotlin
class UserRepositoryTest {
private lateinit var repository: UserRepository
private lateinit var database: Database
@BeforeTest
fun setup() {
database = createTestDatabase()
repository = UserRepository(database)
}
@AfterTest
fun teardown() {
database.close()
}
@Test
fun testGetUser() {
// 测试逻辑
}
}Kotest 框架
功能更丰富的测试框架,提供多种测试风格。
依赖配置
toml
[versions]
kotest = "5.9.1"
[libraries]
kotest-framework = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }测试风格
kotlin
// String Spec 风格
class UserServiceTest : StringSpec({
"should create user successfully" {
val user = service.createUser("Alice")
user.name shouldBe "Alice"
}
"should throw error for empty name" {
shouldThrow<IllegalArgumentException> {
service.createUser("")
}
}
})
// Fun Spec 风格
class CalculatorTest : FunSpec({
test("addition works") {
calculator.add(2, 3) shouldBe 5
}
context("division") {
test("normal division") {
calculator.divide(6, 3) shouldBe 2
}
test("division by zero throws") {
shouldThrow<ArithmeticException> {
calculator.divide(1, 0)
}
}
}
})Matchers
kotlin
import io.kotest.matchers.*
import io.kotest.matchers.collections.*
import io.kotest.matchers.string.*
@Test
fun testMatchers() {
// 数值 matchers
result shouldBe 42
count shouldBeGreaterThan 10
price shouldBeLessThan 100.0
// 字符串 matchers
name shouldStartWith "Alice"
email shouldContain "@"
message shouldMatch Regex("\\d+")
// 集合 matchers
list shouldContain user
list shouldHaveSize 3
list shouldContainExactly listOf(1, 2, 3)
// 类型 matchers
obj shouldBeInstanceOf<User>()
value.shouldNotBeNull()
}MockK (仅 JVM/Android)
JVM 平台的 Mock 框架。
基础用法
kotlin
// androidTest / jvmTest
import io.mockk.*
class UserRepositoryTest {
@Test
fun testWithMock() {
val api = mockk<UserApi>()
// 设置 mock 行为
coEvery { api.fetchUser("123") } returns User("123", "Alice")
val repository = UserRepository(api)
val user = runBlocking { repository.getUser("123") }
assertEquals("Alice", user.name)
// 验证调用
coVerify(exactly = 1) { api.fetchUser("123") }
}
}跨平台 Fake 对象
Mock 库支持有限,推荐使用 Fake 对象:
kotlin
// commonTest
class FakeUserApi : UserApi {
var shouldFail = false
var responseDelay = 0L
val callHistory = mutableListOf<String>()
override suspend fun fetchUser(id: String): User {
callHistory.add("fetchUser($id)")
if (responseDelay > 0) {
delay(responseDelay)
}
if (shouldFail) {
throw IOException("Network error")
}
return User(id, "Test User")
}
}
class UserRepositoryTest {
@Test
fun testWithFake() = runTest {
val api = FakeUserApi()
val repository = UserRepository(api)
val user = repository.getUser("123")
assertEquals("Test User", user.name)
assertEquals(listOf("fetchUser(123)"), api.callHistory)
}
@Test
fun testNetworkError() = runTest {
val api = FakeUserApi().apply {
shouldFail = true
}
val repository = UserRepository(api)
assertFailsWith<IOException> {
repository.getUser("123")
}
}
}数据驱动测试
参数化测试
kotlin
// Kotest
class CalculatorTest : StringSpec({
listOf(
2 to 3 to 5,
10 to 5 to 15,
-1 to 1 to 0
).forEach { (input, expected) ->
"adding ${input.first} and ${input.second} should equal $expected" {
calculator.add(input.first, input.second) shouldBe expected
}
}
})
// kotlin.test
class MathTest {
@Test
fun testMultipleInputs() {
val testCases = listOf(
Triple(2, 3, 6),
Triple(4, 5, 20),
Triple(0, 10, 0)
)
testCases.forEach { (a, b, expected) ->
assertEquals(expected, multiply(a, b),
"multiply($a, $b) should equal $expected")
}
}
}测试覆盖率
Kover 配置
kotlin
// build.gradle.kts
plugins {
id("org.jetbrains.kotlinx.kover") version "0.8.3"
}
kover {
reports {
total {
xml {
onCheck = true
}
html {
onCheck = true
}
}
}
}生成报告
bash
./gradlew koverHtmlReport
# 报告位于 build/reports/kover/html/index.html平台特定测试
Android Instrumented Tests
kotlin
// androidInstrumentedTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AndroidStorageTest {
@Test
fun testSharedPreferences() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val storage = AndroidStorage(context)
storage.save("key", "value")
assertEquals("value", storage.load("key"))
}
}iOS Tests
kotlin
// iosTest
import kotlin.test.Test
import platform.Foundation.NSUserDefaults
class IosStorageTest {
@Test
fun testUserDefaults() {
val storage = IosStorage()
storage.save("key", "value")
assertEquals("value", storage.load("key"))
}
}测试策略
测试金字塔
E2E Tests (5%)
↑
Integration Tests (15%)
↑
Unit Tests (80%)测试分类
单元测试 (commonTest):
- 纯业务逻辑
- 数据转换
- 工具函数
集成测试 (platformTest):
- API 集成
- 数据库操作
- 平台特定功能
E2E 测试 (UI Tests):
- 用户流程
- 跨页面交互
最佳实践
✅ 实践 1:共享测试代码
kotlin
// commonTest
abstract class StorageTest {
abstract fun createStorage(): Storage
@Test
fun testSaveAndLoad() {
val storage = createStorage()
storage.save("key", "value")
assertEquals("value", storage.load("key"))
}
}
// androidTest
class AndroidStorageTest : StorageTest() {
override fun createStorage() = AndroidStorage(context)
}
// iosTest
class IosStorageTest : StorageTest() {
override fun createStorage() = IosStorage()
}✅ 实践 2:使用 Fake 替代 Mock
Fake 对象跨平台兼容性更好:
kotlin
interface Logger {
fun log(message: String)
}
// 测试用 Fake
class FakeLogger : Logger {
val logs = mutableListOf<String>()
override fun log(message: String) {
logs.add(message)
}
}✅ 实践 3:测试命名规范
kotlin
// ✅ 清晰的测试名称
@Test
fun `getUserById should return user when id exists`()
@Test
fun `getUserById should throw NotFoundException when id does not exist`()
// ❌ 模糊的测试名称
@Test
fun test1()
@Test
fun testGetUser()✅ 实践 4:独立测试
kotlin
// ✅ 每个测试独立
@BeforeTest
fun setup() {
database = createTestDatabase() // 每次创建新实例
}
// ❌ 测试间共享状态
companion object {
val sharedDatabase = createDatabase() // 危险
}选择合适的测试框架和策略,可以确保 KMP 项目在所有平台上的质量和稳定性。