Skip to content

单元测试策略

单元测试是保证 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() // 危险
}

通过完善的单元测试,可以确保共享代码在所有平台上的一致性和可靠性。