ListView+Filterable适配的Espresso测试:如何统一注册IdlingResource?
我之前也遇到过这个坑——Espresso默认只盯着主线程的操作,而Filter的performFiltering是在后台线程跑的,所以测试经常会在过滤完成前就执行断言,导致失败。要全局解决这个问题,咱们可以搞一个通用的IdlingResource来跟踪所有Filter的执行状态,不用每个Filter都单独写等待逻辑,一步到位。
实现全局Filter IdlingResource的步骤
1. 编写全局的Filter状态跟踪IdlingResource
先写一个FilterIdlingResource类,实现Espresso的IdlingResource接口,用来统一跟踪所有正在执行的Filter:
class FilterIdlingResource : IdlingResource { // 用原子类保证线程安全(Filter的performFiltering在后台线程,publishResults在主线程) private val activeFilters = AtomicInteger(0) private var resourceCallback: IdlingResource.ResourceCallback? = null // 当Filter开始执行时调用 fun incrementActiveFilters() { activeFilters.incrementAndGet() } // 当Filter执行完成时调用 fun decrementActiveFilters() { val count = activeFilters.decrementAndGet() if (count == 0) { resourceCallback?.onTransitionToIdle() } } override fun getName(): String = "FilterIdlingResource" override fun isIdleNow(): Boolean = activeFilters.get() == 0 override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { this.resourceCallback = callback } }
2. 封装BaseFilter统一处理状态上报
为了不用修改每个Filter的逻辑,咱们写一个BaseFilter基类,让项目里所有实现Filterable的适配器的Filter都继承它,自动把Filter的执行状态上报给上面的IdlingResource:
abstract class BaseFilter(private val idlingResource: FilterIdlingResource) : Filter() { override fun performFiltering(constraint: CharSequence?): FilterResults { // 通知IdlingResource有Filter开始执行 idlingResource.incrementActiveFilters() return try { // 子类实现实际过滤逻辑 onPerformFiltering(constraint) } finally { // 注意:performFiltering在后台线程,不能直接decrement,要等主线程的publishResults完成后再减 } } override fun publishResults(constraint: CharSequence?, results: FilterResults?) { try { // 子类实现结果发布逻辑 onPublishResults(constraint, results) } finally { // 通知IdlingResourceFilter执行完成 idlingResource.decrementActiveFilters() } } // 子类需要实现的实际过滤方法 abstract fun onPerformFiltering(constraint: CharSequence?): FilterResults // 子类需要实现的结果发布方法 abstract fun onPublishResults(constraint: CharSequence?, results: FilterResults?) }
3. 全局注册IdlingResource
在你的测试基类或者每个测试类的@Before方法里,注册这个IdlingResource到Espresso,同时把它传递给所有适配器的BaseFilter:
class ListViewFilterTest { private val filterIdlingResource = FilterIdlingResource() @Before fun setUp() { // 注册IdlingResource,让Espresso感知Filter的执行状态 Espresso.registerIdlingResources(filterIdlingResource) // 把IdlingResource传递给适配器的Filter(根据你的项目结构调整,比如通过依赖注入或者全局实例获取) // 示例:yourListViewAdapter.filter = object : BaseFilter(filterIdlingResource) { ... } } @After fun tearDown() { // 测试结束后注销,避免影响其他测试用例 Espresso.unregisterIdlingResources(filterIdlingResource) } // 测试过滤逻辑 @Test fun whenFilteringListView_shouldShowCorrectResults() { // 执行过滤操作,比如输入搜索文本 onView(withId(R.id.search_edit_text)).perform(typeText("android")) // 现在Espresso会自动等待Filter执行完成,再执行断言 onView(withId(R.id.list_view)).check(matches(hasChildCount(3))) } }
4. 适配现有Filter(无需修改继承关系)
如果项目里已经有很多Filter不想改继承关系,可以用代理模式包装现有Filter:
fun wrapFilter(originalFilter: Filter, idlingResource: FilterIdlingResource): Filter { return object : Filter() { override fun performFiltering(constraint: CharSequence?): FilterResults { idlingResource.incrementActiveFilters() return originalFilter.performFiltering(constraint) } override fun publishResults(constraint: CharSequence?, results: FilterResults?) { originalFilter.publishResults(constraint, results) idlingResource.decrementActiveFilters() } } }
然后在适配器的getFilter()方法里返回包装后的Filter:
override fun getFilter(): Filter { return wrapFilter(originalFilterInstance, filterIdlingResource) }
这样不用修改原有Filter的代码,也能实现全局跟踪。
内容的提问来源于stack exchange,提问作者busylee
相关产品推荐
相关产品推荐

