渐进式编程课
KMP 目录
第 12 课2026.07.28

如果一个类在内部直接创建 Ktor Client 或 Repository,它会与具体实现紧密绑定,测试时也必须访问真实网络。构造器依赖注入让外部决定使用真实实现还是测试替身,Android 和 iOS 也能按各自入口组装同一套共享业务。

第 12 课:用构造器注入共享依赖

  • 日期:2026-07-28
  • 课程序号:12
  • 知识点:通过接口与构造器传入依赖,使共享代码可替换、可测试

用途

如果一个类在内部直接创建 Ktor Client 或 Repository,它会与具体实现紧密绑定,测试时也必须访问真实网络。构造器依赖注入让外部决定使用真实实现还是测试替身,Android 和 iOS 也能按各自入口组装同一套共享业务。

核心概念

  • 依赖者只面向接口,不直接创建具体实现。
  • 依赖通过构造器传入,类的必需条件一目了然。
  • 生产环境传入真实 Repository,测试环境传入 Fake。
  • 先掌握手动注入;项目复杂后再评估是否需要 DI 框架。

示例代码

先定义共享层依赖的接口:

kotlin
interface UserRepository {
    suspend fun loadUser(id: Long): DataResult<User>
}

用例通过构造器接收接口,并负责业务需要的转换:

kotlin
class GetUserLabel(
    private val repository: UserRepository,
) {
    suspend operator fun invoke(id: Long): DataResult<String> {
        return when (val result = repository.loadUser(id)) {
            is DataResult.Success -> {
                DataResult.Success(result.value.displayName)
            }

            is DataResult.Failure -> result
        }
    }
}

生产代码可以传入真实实现:

kotlin
val repository: UserRepository = RealUserRepository(remoteDataSource)
val getUserLabel = GetUserLabel(repository)

测试时传入不访问网络的 Fake:

kotlin
class FakeUserRepository(
    private val result: DataResult<User>,
) : UserRepository {
    override suspend fun loadUser(id: Long): DataResult<User> {
        return result
    }
}

测试使用 runTest 时,需要在 commonTest 加入协程测试依赖:

kotlin
commonTest.dependencies {
    implementation(libs.kotlinx.coroutines.test)
}

commonTest/GetUserLabelTest.kt:

kotlin
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals

class GetUserLabelTest {
    @Test
    fun returnsDisplayNameFromRepository() = runTest {
        val repository = FakeUserRepository(
            DataResult.Success(
                User(id = 1L, displayName = "Ada"),
            ),
        )
        val useCase = GetUserLabel(repository)

        val result = useCase(1L)

        assertEquals(DataResult.Success("Ada"), result)
    }
}

测试只验证共享业务,不依赖服务器、Android SDK 或 iOS API。

5 分钟练习

新增一个测试:让 FakeUserRepository 返回 DataError.Network,验证 GetUserLabel 原样保留失败结果。

参考答案

kotlin
@Test
fun keepsRepositoryFailure() = runTest {
    val repository = FakeUserRepository(
        DataResult.Failure(DataError.Network),
    )
    val useCase = GetUserLabel(repository)

    val result = useCase(1L)

    assertEquals(
        DataResult.Failure(DataError.Network),
        result,
    )
}

用例没有猜测或改写错误来源,因此数据层归类的失败语义可以沿调用链稳定传递。

与上一课的联系

上一课定义了 DataResult 来表达成功和失败;本课通过构造器注入 Repository,并用 Fake 同时验证这两个结果分支,为后续组装完整共享业务做好准备。