第4章:创建基类 Fixture——抽象夹具、泛型设计、状态接口、初始化方法
本课程进入核心设计层:定义一个可复用的抽象夹具基类,通过泛型与状态接口约束,让所有具体的 ViewModel 状态注入夹具能够统一管理初始化、状态变更和测试断言。框架式夹具能让测试代码减少 40% 的模板代码,同时提升类型安全。
4.1 抽象夹具的设计原理
传统测试中,每个 ViewModel 的状态都需要单独创建「setup – assign – verify」代码块,状态错写或漏赋值的编译期错误难以捕获。抽象夹具基类采用 模板方法 + 泛型 模式:
- 泛型参数
T : ViewModelState— 约束所有可注入状态必须实现统一接口。 - 抽象方法
createInitialState()— 子类负责返回该 ViewModel 的初始状态实例。 - 公共方法
injectInto(viewModel)— 基类实现状态注入逻辑。
核心收益: 任何新增 ViewModel 只需编写状态类并实现接口 + 继承夹具基类完成初始状态定义,即可获得通用的“重置 → 注入 → 验证”能力,杜绝重复的 LiveData/StateFlow 赋值代码。
4.2 状态接口定义
状态接口作为所有 ViewModel 状态的契约,通常包含三个基本字段,用于描述 UI 的通用生命周期:
| 字段 | 类型 | 说明 |
|---|---|---|
loading | Boolean | 是否处于加载中 |
error | String? | 错误信息,null 表示无错误 |
data | T? | 业务数据占位,具体类型由子接口决定 |
Kotlin 实现示例如下:
interface ViewModelState<T> {
val loading: Boolean
val error: String?
val data: T?
}
// 具体业务状态
data class LoginState(
override val loading: Boolean = false,
override val error: String? = null,
override val data: String? = null, // 使用 String 作为示例数据
val username: String = "",
val password: String = ""
) : ViewModelState<String>
4.3 基类 Fixture 泛型设计
抽象夹具基类 ViewModelFixture 使用泛型 T : ViewModelState,并提供 var currentState: T 作为内部状态持有变量。以下是完整实现:
abstract class ViewModelFixture<T : ViewModelState<*>> {
protected var currentState: T? = null
// 子类必须实现:返回初始状态
abstract fun createInitialState(): T
// 子类可选重写:在注入前做的额外准备
open fun onBeforeInject(state: T) {}
// 核心注入方法:将状态写入 ViewModel 内部 StateFlow
fun injectInto(viewModel: BaseViewModel<T>) {
val state = currentState ?: createInitialState()
onBeforeInject(state)
viewModel._uiState.value = state
currentState = state
}
// 快速重置到初始状态
fun reset() {
currentState = createInitialState()
}
// 链式修改状态(简化测试写法)
fun update(block: T.() -> T) {
val base = currentState ?: createInitialState()
currentState = base.block()
}
}
4.4 配置编写与 Gralde 依赖
如果使用专门的 fixture 模块(如 :test:fixtures),在模块级 build.gradle.kts 中添加如下依赖:
// 测试夹具模块依赖
dependencies {
api(kotlin("test"))
api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
// 可选:添加 StateFlow testing 支持
api("app.cash.turbine:turbine:1.0.0")
}
调试技巧: 在基类
injectInto 中加入 println("[Fixture] injected state: $state"),运行时可通过 Logcat 快速定位状态是否被正确覆盖。配合 Turbine 库 testScope.runTest 可原子化验证 StateFlow 发射。
4.5 常见错误与避坑
- 未调用
reset()— 多次测试共享同一个 fixture 实例时,currentState会保留上一个测试的修改值。建议在每个@Before中调用fixture.reset()。 - 状态接口泛型污染 — 若
ViewModelState本身携带泛型T,子类继承时必须明确指定具体类型,否则编译器会无法推断。 - 重写
onBeforeInject时忘记调用super.onBeforeInject()— 若基类将来在onBeforeInject中添加日志等逻辑,子类覆盖时需调用 super 方法。
4.6 性能与稳定性优化
- 避免在
createInitialState()中进行 IO 操作 — 该方法在测试中高频调用,应保持纯内存对象创建。 - 使用
inline优化更新操作 — 对于update方法,可添加inline和crossinline修饰,减少 lambda 对象分配(在极端大量用例下有效)。 - 状态快照拍摄 — 基类可增加
snapshot()方法返回当前状态的不可变副本,用于测试中多步变更后对比。
⚠ 稳定性注意: 不要将 fixture 设计为单例。多个测试用例如果共享同一个 fixture 实例且没有正确重置,会导致状态污染,产生难以追踪的“闪烁测试”。每个测试方法都应创建新的 fixture 实例(或在
@Before 中 reset())。