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 单元测试核心流程 1. 配置测试环境 @Rule InstantTaskExecutorRule 2. 创建ViewModel 初始化 & 注册观察者 3. 触发操作 setValue/postValue 4. 验证结果 assertEquals 检查值 5. 清理资源 removeObserver 6. 验证观察者 通知次数/顺序 关键原则 InstantTaskExecutorRule 让异步变同步 → 测试可预测、可重复

常见问题与避坑指南

写LiveData测试的时候,有几个坑我反复踩过,今天一并告诉你:

  • 忘记注册Rule:这是最常见的错误。没有InstantTaskExecutorRule,LiveData的postValue永远不会执行,测试就会一直卡着或者报错。
  • 观察者没移除导致内存泄漏:用observeForever一定要记得在测试结束前removeObserver。我刚开始写的时候经常忘,后来养成了在@After里统一清理的习惯。
  • 混淆setValue和postValue的行为:在测试环境里,有了InstantTaskExecutorRule,两者行为一样。但生产环境不一样,postValue是异步的,如果你在测试里依赖了postValue的同步行为,上线后可能会出问题。

我的建议:在ViewModel里尽量用setValue,除非你明确需要在后台线程更新数据。这样测试和生产的行为更一致,少踩坑。

总结一下

LiveData测试的核心就三件事:配好Rule、注册观察者、验证数据。你只要记住这三点,大部分场景都能覆盖。

我个人习惯在项目里给每个ViewModel都写一套完整的LiveData测试,包括正常数据更新、边界值、重复赋值等场景。虽然写的时候多花点时间,但后期维护起来真的省心很多。

好了,这一章的内容就到这里。记住,测试不是负担,而是你代码质量的护城河。


公众号:蓝海资料掘金营,微信deep3321