17、LiveData的测试:JUnit测试LiveData、InstantTaskExecutorRule的使用、测试数据更新与观察
说实话,很多开发者写LiveData用得挺溜,但一到写测试就犯怵。我刚开始带团队的时候,发现大家普遍有个误区——觉得LiveData测试很麻烦,干脆就不写了。结果呢?线上出了几次数据状态不对的bug,排查起来那叫一个痛苦。
其实LiveData的测试没那么玄乎。你想想看,它本质上就是个被观察的数据容器。我们只需要验证两件事:数据有没有正确更新,观察者有没有正确收到通知。今天我就带你把这套东西捋清楚。
为什么LiveData测试需要特殊处理?
LiveData默认是跟Android生命周期绑定的。它会在主线程上派发数据,而且只在活跃状态下才通知观察者。这就给单元测试带来了麻烦——JUnit跑在测试线程上,没有主线程Looper,也没有生命周期概念。
我在项目中遇到过这样的情况:写了个简单的LiveData测试,结果一直报错说"Can't create handler inside thread that has not called Looper.prepare()"。当时我还纳闷,明明逻辑没问题啊。后来才意识到,是测试环境缺少了Android运行时的那一套基础设施。
要解决这个问题,我们需要两个关键工具:
- InstantTaskExecutorRule:让LiveData的异步操作变成同步执行
- getOrAwaitValue():一个常用的扩展函数,用于获取LiveData的值
InstantTaskExecutorRule 到底是什么?
这个Rule是AndroidX Test库提供的。它的作用说白了就一句话:把LiveData的异步行为变成同步。
正常情况下,LiveData.postValue()会把任务post到主线程队列里,等Looper轮询到才执行。但在测试环境里,没有主线程Looper在跑,所以任务就永远得不到执行。
InstantTaskExecutorRule会替换掉LiveData内部的执行器,让所有任务在当前线程立即执行。这样一来,你调用setValue()或postValue()之后,观察者立刻就能收到通知。
核心要点:使用InstantTaskExecutorRule后,LiveData的setValue()和postValue()行为完全一致,都是同步执行。但在生产代码中,postValue()仍然是异步的,这点要注意区分。
实战:写一个完整的LiveData测试
我们先从最简单的例子开始。假设有一个ViewModel,里面有个LiveData用来显示用户姓名:
class UserViewModel : ViewModel() {
private val _userName = MutableLiveData<String>()
val userName: LiveData<String> get() = _userName
fun updateName(name: String) {
_userName.value = name
}
}
对应的测试代码长这样:
class UserViewModelTest {
// 关键:注册这个Rule
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
viewModel = UserViewModel()
}
@Test
fun `updateName should update userName LiveData`() {
// 先观察LiveData
val observer = Observer<String> { name ->
println("收到更新: $name")
}
viewModel.userName.observeForever(observer)
// 更新数据
viewModel.updateName("张三")
// 验证结果
assertEquals("张三", viewModel.userName.value)
// 清理
viewModel.userName.removeObserver(observer)
}
}
嗯,这里要注意一点。我习惯用observeForever()而不是observe(),因为后者需要LifecycleOwner,在测试里处理起来比较麻烦。observeForever()会一直观察,直到你手动移除。
测试数据更新的几种场景
在实际项目中,LiveData的数据更新往往不是简单的赋值。我总结了三种常见场景:
| 场景 | 说明 | 测试要点 |
|---|---|---|
| 直接赋值 | 通过setValue()更新 | 验证值是否正确,观察者是否收到通知 |
| 异步更新 | 通过postValue()在后台线程更新 | 使用InstantTaskExecutorRule后,postValue也变成同步 |
| 数据转换 | 使用Transformations.map或switchMap | 验证转换逻辑是否正确执行 |
来看一个带数据转换的例子:
class UserProfileViewModel : ViewModel() {
private val _userId = MutableLiveData<String>()
val userProfile: LiveData<UserProfile> = Transformations
.switchMap(_userId) { id ->
fetchUserProfile(id)
}
fun loadUser(id: String) {
_userId.value = id
}
private fun fetchUserProfile(userId: String): LiveData<UserProfile> {
val result = MutableLiveData<UserProfile>()
// 模拟网络请求
result.value = UserProfile(userId, "用户$userId")
return result
}
}
测试代码:
@Test
fun `loadUser should trigger profile update via switchMap`() {
val viewModel = UserProfileViewModel()
val observer = Observer<UserProfile> { profile ->
println("收到profile: ${profile.name}")
}
viewModel.userProfile.observeForever(observer)
viewModel.loadUser("123")
val profile = viewModel.userProfile.value
assertNotNull(profile)
assertEquals("123", profile?.id)
assertEquals("用户123", profile?.name)
viewModel.userProfile.removeObserver(observer)
}
个人小技巧:我习惯在测试里用observeForever配合removeObserver,而不是用getOrAwaitValue()。虽然getOrAwaitValue写起来更简洁,但observeForever能让你更清楚地看到观察者的行为,排查问题时更有掌控感。
测试观察者是否收到通知
有时候我们不光要验证值对不对,还要验证观察者有没有被正确通知。比如,数据没变化时,观察者不应该被重复通知。
我曾经踩过一个坑:有个页面在数据刷新时,观察者被调用了两次,导致UI闪了一下。后来排查发现,是LiveData的setValue被调用了两次,但两次的值是一样的。按理说,值没变就不应该通知观察者,但当时代码里用了postValue,导致了一些奇怪的行为。
测试代码可以这样写:
@Test
fun `observer should not be notified when same value is set`() {
val liveData = MutableLiveData<String>()
var notifyCount = 0
val observer = Observer<String> {
notifyCount++
}
liveData.observeForever(observer)
liveData.value = "Hello"
assertEquals(1, notifyCount)
// 设置相同的值
liveData.value = "Hello"
// 注意:LiveData默认会通知,即使值相同
// 如果需要去重,要自己处理
assertEquals(2, notifyCount)
liveData.removeObserver(observer)
}
注意:LiveData默认不会对值做去重处理。每次调用setValue()都会通知观察者,不管值有没有变化。如果你需要去重,可以在ViewModel里自己加判断,或者使用Kotlin的distinctUntilChanged()操作符。
用SVG梳理测试流程
下面这张图展示了LiveData测试的核心流程,我把它画出来方便你理解:
常见问题与避坑指南
写LiveData测试的时候,有几个坑我反复踩过,今天一并告诉你:
- 忘记注册Rule:这是最常见的错误。没有InstantTaskExecutorRule,LiveData的postValue永远不会执行,测试就会一直卡着或者报错。
- 观察者没移除导致内存泄漏:用observeForever一定要记得在测试结束前removeObserver。我刚开始写的时候经常忘,后来养成了在@After里统一清理的习惯。
- 混淆setValue和postValue的行为:在测试环境里,有了InstantTaskExecutorRule,两者行为一样。但生产环境不一样,postValue是异步的,如果你在测试里依赖了postValue的同步行为,上线后可能会出问题。
我的建议:在ViewModel里尽量用setValue,除非你明确需要在后台线程更新数据。这样测试和生产的行为更一致,少踩坑。
总结一下
LiveData测试的核心就三件事:配好Rule、注册观察者、验证数据。你只要记住这三点,大部分场景都能覆盖。
我个人习惯在项目里给每个ViewModel都写一套完整的LiveData测试,包括正常数据更新、边界值、重复赋值等场景。虽然写的时候多花点时间,但后期维护起来真的省心很多。
好了,这一章的内容就到这里。记住,测试不是负担,而是你代码质量的护城河。
公众号:蓝海资料掘金营,微信deep3321