16、IdlingResource与异步测试:从原理到实战
各位同学,今天我们来聊聊异步测试里最核心的一个话题——IdlingResource。说实话,我刚接触Android测试那会儿,最头疼的就是异步操作。网络请求、数据库读写、动画播放……这些玩意儿在测试里就像个“黑箱”,你永远不知道它什么时候结束。后来我明白了,不是测试框架不行,是我没用好IdlingResource。
16.1 为什么需要IdlingResource?
先问大家一个问题:Espresso是怎么判断UI是否空闲的?
默认情况下,Espresso会监控主线程的消息队列。当消息队列空了,它就认为“嗯,UI空闲了,可以执行下一个操作了”。但问题来了——网络请求是异步的,数据库操作也是异步的,它们不会往主线程消息队列里塞消息。Espresso根本不知道这些任务还在跑。
结果就是:你明明发了个网络请求,Espresso却以为已经结束了,直接去检查结果。这时候UI还没更新呢,测试当然会失败。
我在项目里就踩过这个坑。有一次写登录测试,点击登录按钮后直接检查TextView是否显示“登录成功”。结果呢?网络请求还没回来,测试就断言失败了。后来排查了半天,才发现是异步任务没等完。
16.2 IdlingResource的工作原理
说白了,IdlingResource就是一个“状态监听器”。它有三个核心方法:
getName():返回资源名称,方便调试isIdleNow():当前是否空闲。如果返回true,Espresso就认为可以继续registerIdleTransitionCallback():当状态从“忙碌”变为“空闲”时,通知Espresso
Espresso会定期轮询所有注册的IdlingResource。只要有一个返回false,它就等着。直到全部返回true,才执行下一个测试步骤。
我画了个流程图,方便大家理解:
16.3 注册与取消IdlingResource
使用IdlingResource之前,必须先注册到Espresso的框架里。我习惯在@Before方法里注册,在@After里取消。这样每个测试用例都是干净的,不会互相影响。
class LoginViewModelTest {
private lateinit var idlingResource: CountingIdlingResource
@Before
fun setUp() {
idlingResource = CountingIdlingResource("network_call")
Espresso.registerIdlingResources(idlingResource)
}
@After
fun tearDown() {
Espresso.unregisterIdlingResources(idlingResource)
}
@Test
fun testLogin() {
// 测试逻辑
}
}
16.4 自定义IdlingResource
Espresso自带的CountingIdlingResource已经能满足大部分场景了。但有些特殊需求,比如等待某个自定义View加载完成,就得自己动手写。
我记得有一次,项目里用了一个第三方图表库,加载数据需要时间。Espresso不知道它在忙,测试总是失败。后来我写了个自定义IdlingResource,监听图表库的内部状态。
class ChartIdlingResource(private val chartView: ChartView) : IdlingResource {
private var callback: IdlingResource.ResourceCallback? = null
override fun getName(): String = "ChartIdlingResource"
override fun isIdleNow(): Boolean {
val idle = chartView.isDataLoaded()
if (idle) {
callback?.onTransitionToIdle()
}
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}
}
这里有个关键点:isIdleNow()返回true时,一定要调用callback.onTransitionToIdle()。否则Espresso可能收不到通知,一直卡在那。
16.5 测试网络请求与数据库操作
网络请求和数据库操作是异步测试的重灾区。我给大家总结一下常见的做法:
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| Retrofit网络请求 | 使用CountingIdlingResource,在请求开始前increment,结束后decrement | 注意回调线程,确保decrement在正确的地方调用 |
| Room数据库操作 | 同样用CountingIdlingResource,包裹LiveData或Flow的观察 | Room的异步查询默认在后台线程,需要等数据回调到主线程 |
| 协程+网络请求 | 可以用runBlockingTest,或者自定义IdlingResource监听协程状态 | 协程的调度器要替换为TestDispatcher |
拿Retrofit举例,我一般会写一个拦截器,自动管理IdlingResource的状态:
class IdlingResourceInterceptor(
private val idlingResource: CountingIdlingResource
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
idlingResource.increment()
return try {
chain.proceed(chain.request())
} finally {
idlingResource.decrement()
}
}
}
这样每个网络请求都会自动被IdlingResource监控,测试代码里不用手动管理。省心不少。
16.6 测试动画与延迟
动画和延迟是另一个坑。Espresso默认会等待动画结束,但有些动画是无限循环的,或者延迟时间太长。这时候就需要特殊处理。
我的做法是:在测试环境里禁用动画。在@Before里设置:
@Before
fun disableAnimations() {
val animatorDurationScale = Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f
)
// 设置为0,禁用动画
Settings.Global.putFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
0f
)
}
但注意,这个操作需要系统权限。在模拟器上没问题,真机可能不行。另一种方案是用Espresso的IdlingPolicies:
@Before
fun setUpIdlingPolicies() {
IdlingPolicies.setMasterPolicyTimeout(10, TimeUnit.SECONDS)
IdlingPolicies.setIdlingResourceTimeout(5, TimeUnit.SECONDS)
}
这样即使动画或延迟稍微超时,测试也不会立刻失败。但别设太大,否则测试跑起来太慢。
repeatCount = infinite。解决方案是在测试环境里把动画替换成一次性动画,或者干脆禁用。
16.7 实战:组合使用多个IdlingResource
实际项目中,往往同时有网络请求、数据库操作和动画。这时候需要注册多个IdlingResource。Espresso会等所有资源都空闲了才继续。
我习惯写一个工具类来管理:
object TestIdlingResourceManager {
private val networkResource = CountingIdlingResource("network")
private val dbResource = CountingIdlingResource("database")
private val animResource = CountingIdlingResource("animation")
fun registerAll() {
Espresso.registerIdlingResources(networkResource, dbResource, animResource)
}
fun unregisterAll() {
Espresso.unregisterIdlingResources(networkResource, dbResource, animResource)
}
fun getNetworkResource() = networkResource
fun getDbResource() = dbResource
fun getAnimResource() = animResource
}
然后在测试基类里统一注册和取消。每个测试用例只管自己的业务逻辑,不用操心IdlingResource的管理。
好了,关于IdlingResource的内容就讲到这里。异步测试其实没那么可怕,关键是把“等待”这件事交给框架,而不是自己写Thread.sleep。相信我,用好了IdlingResource,你的测试会稳定很多。