第1章 夹具分层设计原则
在 Room 数据库的测试场景中,夹具(Fixture)用于构造可预测的数据上下文。直接在每个测试方法中手工创建数据会导致职责混杂、重复代码和难以维护的测试套件。本章引入四项核心设计原则,为后续的夹具分层架构奠定基础。这四项原则分别是:单一职责、依赖反转、可复用性与可测试性。
关键认知:分层不是简单地把代码拆文件,而是通过原则驱动的边界定义,让每一层只关注一个变化维度。
原则一:单一职责
每个夹具类或方法只负责一种数据实体的构造或一种测试上下文的准备。例如:
- UserFixture 只生成 User 实体,不关心 Order 或 Product。
- OrderSetupFixture 只负责组合一个完整的订单环境,不嵌入断言逻辑。
边界清晰后,当数据库 schema 调整时,只需修改对应的单一夹具,不会波及无关测试。
常见陷阱:在同一个夹具中既创建用户又创建订单,还执行验证。这会破坏复用性,并使测试依赖隐含执行顺序。
原则二:依赖反转
高层夹具不应直接依赖 Room DAO 的具体实现,而应依赖抽象接口。通过依赖注入(DI),测试时可以用模拟(Mock)或内存数据库替换真实实现。
典型接口定义(使用 Kotlin 接口,避免具体 DAO 耦合):
interface UserRepository {
suspend fun insert(user: User): Long
suspend fun queryById(id: Long): User?
}
class UserFixture(private val repo: UserRepository) {
suspend fun createDefaultUser(): User = User(
name = "test_user",
email = "test@example.com"
).also { repo.insert(it) }
}
这样,测试时可以注入 FakeUserRepository,无需启动真正的 Room 数据库。
原则三:可复用性
基础夹具设计为无状态、参数化,可以在多个测试类间共享。例如一个 UserBaseFixture 提供 insertUsers(vararg users) 和 clearAll() 方法,业务夹具继承或组合它来构建复杂场景。
可复用性度量的简单指标:
| 衡量维度 | 好 | 差 |
|---|---|---|
| 跨模块引用次数 | ≥ 3 次 | 仅 1 次 |
| 参数化程度 | 支持 key-value 参数覆盖 | 固定默认值,无法定制 |
| 副作用 | 自动清理,可重复调用 | 残留数据影响后续测试 |
原则四:可测试性
夹具本身应当是可验证的。这意味着夹具的内部逻辑不应隐藏随机性、外部状态或全局单例。通过依赖反转,我们可以为夹具编写独立的单元测试。
示例:测试 UserFixture 是否正确设置了默认邮箱格式:
class UserFixtureTest {
private val fakeRepo = FakeUserRepository()
private val fixture = UserFixture(fakeRepo)
@Test
fun `createDefaultUser should set email with '@'`() = runBlocking {
val user = fixture.createDefaultUser()
assertTrue(user.email.contains("@"))
}
}
这种可测试性反过来又保证了夹具本身的质量,避免“测试的测试”陷阱。
四原则的协同与权衡
在真实项目中,这四项原则有时会相互制约。例如过度追求单一职责可能导致夹具类数量膨胀,影响可复用性;过于依赖反转可能引入不必要的接口抽象。下表总结了常见权衡点:
| 原则 | 力(好处) | 代价 |
|---|---|---|
| 单一职责 | 易于修改,变动影响小 | 类数量增加,需额外组合 |
| 依赖反转 | 测试与实现解耦,支持 mock | 需要 DI 框架或手动注入 |
| 可复用性 | 减少重复代码 | 参数过多时 API 变得复杂 |
| 可测试性 | 验证夹具自身逻辑 | 额外的测试用例维护成本 |
设计决策参考:在项目初期,优先保证依赖反转与可测试性,因为它们是后续可维护性的根基。等测试数据规模增长到出现明显重复时,再通过单一职责和可复用性重构夹具分层。