28、Test KTX:单元测试与UI测试的KTX扩展,简化测试编写。

说实话,写测试这件事,很多Android开发者一开始是抗拒的。我自己也不例外。早些年做项目,总觉得“功能跑通就行了”,测试?那是QA的事。直到有一次,我改了一个看似无关紧要的工具类,结果导致三个页面同时崩溃——嗯,那次上线前的通宵加班,让我彻底明白了测试的重要性。

但写测试确实有门槛。尤其是Android的测试,涉及生命周期、上下文、异步任务,光搭建环境就够折腾的。好在Google推出了Test KTX,说白了就是给测试框架加了一层“语法糖”,让测试代码写起来更像Kotlin,更简洁,也更直观。

Test KTX 到底解决了什么问题?

我们先看一个典型场景。你写了一个ViewModel,里面用到了LiveData。你想测试数据变化时UI是否更新。传统写法是这样的:

@Test
fun testLiveData() {
    val observer = mock(Observer::class.java)
    liveData.observeForever(observer)
    liveData.value = "hello"
    verify(observer).onChanged("hello")
    liveData.removeObserver(observer)
}

这段代码能跑,但每次都要手动注册、移除观察者,很烦。而且一旦忘记移除,还可能造成内存泄漏。我早期项目里就踩过这个坑——测试跑多了,内存直接爆掉。

用Test KTX,可以写成这样:

@Test
fun testLiveData() = liveDataTest {
    val observer = mock(Observer::class.java)
    observeForever(observer)
    emit("hello")
    verify(observer).onChanged("hello")
}

看到了吗?liveDataTest这个作用域函数帮我们自动管理了观察者的生命周期。测试结束后,观察者会被自动移除。这就是KTX的魅力——把繁琐的模板代码藏起来,让你只关注业务逻辑。

核心扩展函数一览

Test KTX 主要提供了以下几个扩展,我按使用频率排个序:

扩展函数 作用 适用场景
liveDataTest 创建LiveData测试作用域,自动管理观察者 ViewModel中LiveData的单元测试
InstantTaskExecutorRule 让LiveData和Room的异步操作变为同步执行 避免测试中的线程问题
createAndroidComputation 在测试环境中模拟Android主线程 需要主线程调用的场景
runOnUiThread 在UI线程上执行代码块 UI测试中的视图操作

我个人最常用的是InstantTaskExecutorRule。为什么?因为LiveData默认是在主线程更新数据的,而单元测试跑在后台线程。不加这个Rule,你十有八九会遇到CalledFromWrongThreadException。我曾经花了一整个下午排查这个错误,最后发现只是少加了一行注解——你说冤不冤?

UI测试中的KTX:让Espresso更Kotlin

UI测试方面,Test KTX 对 Espresso 和 Compose 测试都做了扩展。拿 Espresso 来说,传统写法是:

onView(withId(R.id.button)).perform(click())
onView(withText("提交")).check(matches(isDisplayed()))

用KTX后,可以写成:

onView(withId(R.id.button)).click()
onView(withText("提交")).isDisplayed()

少写几个方法调用,代码读起来更像自然语言。你想想看,.click().perform(click()),哪个更直观?显然是前者。

对于 Compose 测试,Test KTX 提供了 createComposeRule 和一系列语义匹配扩展。比如:

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testComposeButton() {
    composeTestRule.setContent {
        MyButton("点击我")
    }
    composeTestRule.onNodeWithText("点击我").assertIsDisplayed()
}

这里要注意一点:Compose 测试的节点查找是基于语义树的,不是基于ID。如果你用惯了 Espresso 的 withId,刚转过来可能会不习惯。我刚开始写 Compose 测试时,就老想着找ID,结果发现根本找不到——因为 Compose 里没有传统意义上的 View ID。

避坑指南:我曾经踩过的三个坑

坑一:忘记加 @RunWith(AndroidJUnit4::class)

这个注解告诉测试框架要用 Android 的测试运行器。不加的话,InstantTaskExecutorRule 可能不会生效。我有一阵子测试总是莫名其妙地超时,排查了半天才发现是运行器没配对。

坑二:LiveData 的 observeForeverliveDataTest 混用

如果你在 liveDataTest 块里又手动调用了 observeForever,会导致观察者被注册两次。一次是框架帮你注册的,一次是你自己注册的。结果就是回调执行两次,断言可能通过,但逻辑其实是错的。我的建议是:要么全用框架,要么全手动,别混着来。

坑三:UI测试中忘记等待异步操作

Espresso 默认会等待主线程空闲,但如果你有自定义的异步任务(比如协程),它可能等不到。这时候需要用 IdlingResource 或者 Compose 的 waitUntil。我早期一个项目里,网络请求的UI测试总是时好时坏,后来才发现是异步等待没处理好。

知识体系总览

下面这张图是我整理的 Test KTX 核心脉络,你可以对照着看:

Test KTX 单元测试扩展 liveDataTest 作用域 InstantTaskExecutorRule createAndroidComputation UI测试扩展 Espresso 语法糖 Compose 测试扩展 runOnUiThread 核心价值:减少模板代码,提升可读性

实际项目中的最佳实践

说了这么多,到底怎么用?我分享一个我项目里的真实案例。我们有个登录模块,ViewModel 里有个 loginResult 的 LiveData。测试时我需要验证:

  1. 点击登录按钮后,显示加载状态
  2. 网络请求成功后,跳转到主页
  3. 网络请求失败后,显示错误提示

用 Test KTX 写出来是这样的:

@RunWith(AndroidJUnit4::class)
class LoginViewModelTest {

    @get:Rule
    val instantRule = InstantTaskExecutorRule()

    private lateinit var viewModel: LoginViewModel

    @Before
    fun setup() {
        viewModel = LoginViewModel()
    }

    @Test
    fun `login success should navigate to home`() = liveDataTest {
        // 模拟成功响应
        viewModel.login("user", "pass")
        
        // 验证加载状态
        assertThat(viewModel.isLoading.getOrAwaitValue()).isTrue()
        
        // 模拟网络返回
        viewModel.onLoginResult(Result.success(User("test")))
        
        // 验证最终状态
        assertThat(viewModel.loginResult.getOrAwaitValue()).isEqualTo(
            LoginState.Success
        )
    }
}

这里用到了 getOrAwaitValue(),它是 LiveData 测试的一个常用扩展,可以阻塞等待直到 LiveData 有值返回。这个扩展不是 Test KTX 自带的,但配合起来用效果很好。

💡 小技巧: 如果你的测试涉及多个 LiveData 的并发更新,建议用 liveDataTestemit 方法逐个发送值,而不是直接修改 LiveData 的 value。这样可以保证测试的时序可控。
⚠️ 注意: liveDataTest 内部使用了协程的 runBlockingTest,如果你的测试代码里也有协程,要注意避免嵌套阻塞。我遇到过测试死锁的情况,就是因为两个阻塞操作互相等待。

总结一下

Test KTX 不是什么高深的技术,它做的就是一件事:让测试代码更像Kotlin。去掉冗余的模板,保留核心的断言逻辑。如果你还在手写 observeForeverremoveObserver,不妨试试 liveDataTest。相信我,写一次你就会爱上它。

最后说一句:测试不是负担,而是保障。尤其是团队协作的项目,好的测试能帮你省下无数个通宵。嗯,这话是我用加班换来的经验。


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