You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Espresso为代码创建的自定义视图编写UI测试并等待渲染

嘿,刚好碰到过类似的场景,给你分享几个纯Espresso的解决方案,完全不用依赖Robolectric:

1. 先搞定代码生成自定义视图的定位问题

Espresso默认对XML生成的视图支持很好,但动态生成的自定义视图经常因为没有明确的标识导致定位失败。你可以这么做:

  • 给自定义视图设置唯一标识:生成视图时,用setTag()或者setContentDescription()给它加个独有的标记,比如:
    // 自定义视图生成代码里
    val placeCard = PlaceInfoView(context)
    placeCard.setTag(R.id.place_card_tag, "unique_restaurant_card_${place.id}")
    
  • 在测试里用标记定位:结合withTagValue()或者withContentDescription()来精准找到视图:
    onView(allOf(
        withTagValue(equalTo("unique_restaurant_card_123")),
        isDisplayed()
    )).check(matches(isDisplayed()))
    
  • 还可以通过子控件反向定位:如果自定义视图里有带文本的子控件,用hasDescendant()来定位父视图,比如:
    onView(allOf(
        instanceOf(PlaceInfoView::class.java),
        hasDescendant(withText("XX火锅店")),
        isDisplayed()
    ))
    
2. 等待视图渲染完成的两种方案

针对Mapbox地图加载(约2秒)的场景,Espresso有两种靠谱的等待方式:

推荐:用IdlingResource监听异步加载完成

这是Espresso官方推荐的异步等待方案,不会像固定延迟那样导致测试不稳定。比如监听Mapbox地图加载完成:

  1. 实现一个Mapbox专属的IdlingResource:
    class MapboxLoadIdlingResource(private val mapView: MapView) : IdlingResource {
        private var callback: ResourceCallback? = null
        private var isMapLoaded = false
    
        init {
            mapView.getMapAsync { mapboxMap ->
                mapboxMap.addOnMapLoadedCallback {
                    isMapLoaded = true
                    callback?.onTransitionToIdle()
                }
            }
        }
    
        override fun getName() = "MapboxLoadIdlingResource"
        override fun isIdleNow() = isMapLoaded
        override fun registerIdleTransitionCallback(callback: ResourceCallback?) {
            this.callback = callback
        }
    }
    
  2. 在测试的@Before和@After里注册/注销:
    @get:Rule
    val activityRule = ActivityScenarioRule(MainMapActivity::class.java)
    
    @Before
    fun setUp() {
        activityRule.scenario.onActivity { activity ->
            val mapView = activity.findViewById<MapView>(R.id.main_map_view)
            IdlingRegistry.getInstance().register(MapboxLoadIdlingResource(mapView))
        }
    }
    
    @After
    fun tearDown() {
        IdlingRegistry.getInstance().unregisterAll()
    }
    

这样Espresso会自动等待地图加载完成,再执行后续的匹配操作。如果是自定义视图自己的渲染逻辑,你可以给视图加个onRenderComplete回调,然后在回调里标记IdlingResource为空闲状态。

应急:固定延迟等待(不推荐,但好用)

如果暂时没时间写IdlingResource,也可以用Espresso的主线程循环来延迟,比Thread.sleep()更稳定:

// 让主线程循环3秒,保证视图有足够时间渲染
onView(withId(R.id.main_map_view)).perform(object : ViewAction {
    override fun getConstraints() = isDisplayed()
    override fun getDescription() = "Wait for 3 seconds to load place views"
    override fun perform(uiController: UiController, view: View) {
        uiController.loopMainThreadForAtLeast(3000)
    }
})

不过这种方式尽量少用,因为不同设备的加载速度不一样,容易导致测试在某些设备上失败。

3. 完整测试示例

假设你要测试搜索餐厅后,自定义的PlaceInfoView是否正确显示:

@Test
fun testRestaurantPlaceViewIsDisplayedAndInteractive() {
    // 1. 触发搜索操作
    onView(withId(R.id.search_bar)).perform(typeText("火锅店"), pressImeActionButton())

    // 2. Espresso会自动等待Mapbox加载完成(通过IdlingResource)
    // 3. 定位自定义视图并验证
    onView(allOf(
        instanceOf(PlaceInfoView::class.java),
        hasDescendant(withText("XX老火锅")),
        isDisplayed()
    )).check(matches(isDisplayed()))

    // 4. 测试视图交互,比如点击预约按钮
    onView(allOf(
        withId(R.id.book_table_btn),
        isDescendantOfA(instanceOf(PlaceInfoView::class.java))
    )).perform(click())

    // 5. 验证交互结果,比如跳转预约页面
    onView(withId(R.id.book_table_activity_layout)).check(matches(isDisplayed()))
}
4. 常见坑点排查
  • 确认视图已经加入到视图层级:可以用adb shell dumpsys activity your.app.package查看当前视图树,确保自定义视图存在。
  • 如果视图在滚动容器里:需要先用scrollTo()操作把视图滚到可见区域,比如perform(scrollTo())。
  • 自定义视图的异步逻辑:尽量把耗时的渲染逻辑抽成可监听的回调,方便用IdlingResource等待,避免直接在构造函数里做耗时操作。

内容的提问来源于stack exchange,提问作者rozeri dilar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:02:19