单元测试策略
单元测试是保证 KMP 项目质量的关键。合理的测试策略可以确保共享代码在所有平台上正常工作。
测试框架配置
依赖配置
toml
[versions]
kotlin = "2.3.0"
kotlinx-coroutines = "1.9.0"
turbine = "1.2.0"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }kotlin
kotlin {
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
implementation(libs.turbine)
}
}
}基础单元测试
kotlin
// commonTest
import kotlin.test.*
class UserValidatorTest {
private lateinit var validator: UserValidator
@BeforeTest
fun setup() {
validator = UserValidator()
}
@Test
fun testValidEmail() {
assertTrue(validator.isValidEmail("user@example.com"))
}
@Test
fun testInvalidEmail() {
assertFalse(validator.isValidEmail("invalid-email"))
}
@Test
fun testPasswordLength() {
val result = validator.validatePassword("12345")
assertFalse(result.isValid)
assertEquals("Password too short", result.error)
}
}协程测试
runTest 使用
kotlin
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class UserRepositoryTest {
@Test
fun testFetchUser() = runTest {
val repository = UserRepository(FakeApiService())
val user = repository.getUser("123")
assertEquals("Alice", user.name)
}
@Test
fun testConcurrentFetch() = runTest {
val repository = UserRepository(FakeApiService())
val users = List(10) { index ->
async { repository.getUser(index.toString()) }
}.awaitAll()
assertEquals(10, users.size)
}
}时间控制
kotlin
import kotlinx.coroutines.test.*
import kotlinx.coroutines.delay
@Test
fun testWithDelay() = runTest {
var result = false
launch {
delay(1000)
result = true
}
// 快进时间
advanceTimeBy(1000)
assertTrue(result)
}Flow 测试
Turbine 库
kotlin
import app.cash.turbine.test
import kotlinx.coroutines.flow.flow
@Test
fun testFlow() = runTest {
val flow = flow {
emit(1)
emit(2)
emit(3)
}
flow.test {
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
assertEquals(3, awaitItem())
awaitComplete()
}
}
@Test
fun testStateFlow() = runTest {
val repository = UserRepository()
repository.usersFlow.test {
// 初始值
assertEquals(emptyList(), awaitItem())
// 触发更新
repository.loadUsers()
// 验证新值
val users = awaitItem()
assertTrue(users.isNotEmpty())
}
}Mock 对象
手动 Mock
kotlin
class FakeUserRepository : UserRepository {
private val users = mutableMapOf<String, User>()
override suspend fun getUser(id: String): User {
return users[id] ?: throw NotFoundException()
}
override suspend fun saveUser(user: User) {
users[user.id] = user
}
fun addTestUser(user: User) {
users[user.id] = user
}
}
@Test
fun testWithFake() = runTest {
val repository = FakeUserRepository().apply {
addTestUser(User("1", "Alice"))
}
val user = repository.getUser("1")
assertEquals("Alice", user.name)
}expect/actual 测试
共享测试逻辑
kotlin
// commonTest
expect fun createTestDatabase(): Database
class DatabaseTest {
private lateinit var database: Database
@BeforeTest
fun setup() {
database = createTestDatabase()
}
@Test
fun testInsert() = runTest {
database.insertUser(User("1", "Alice"))
val user = database.getUser("1")
assertEquals("Alice", user?.name)
}
}
// androidTest
actual fun createTestDatabase(): Database {
return Database(createInMemoryDriver())
}
// iosTest
actual fun createTestDatabase(): Database {
return Database(createInMemoryDriver())
}最佳实践
✅ 实践 1:使用 Fake 而非 Mock
kotlin
// ✅ Fake 更可控
class FakeNetworkClient : NetworkClient {
var shouldFail = false
var responseDelay = 0L
override suspend fun get(url: String): String {
delay(responseDelay)
if (shouldFail) throw IOException()
return "{\"id\":\"123\"}"
}
}
// ❌ 避免复杂 Mock 库(KMP 支持有限)✅ 实践 2:测试边界条件
kotlin
@Test
fun testEmptyList() {
val result = processUsers(emptyList())
assertTrue(result.isEmpty())
}
@Test
fun testNullValue() {
assertFailsWith<IllegalArgumentException> {
validateUser(null)
}
}✅ 实践 3:独立测试
kotlin
// ✅ 每个测试独立
@BeforeTest
fun setup() {
repository = UserRepository() // 每次创建新实例
}
// ❌ 避免测试间共享状态
companion object {
val sharedRepository = UserRepository() // 危险
}通过完善的单元测试,可以确保共享代码在所有平台上的一致性和可靠性。