UI测试基础与Espresso入门

说实话,很多Android开发者对UI测试的态度是「知道很重要,但就是不想写」。我完全理解——写UI测试确实比写单元测试麻烦,要配环境、要等模拟器启动、还要处理各种异步问题。但我要说一句:如果你从来没写过UI测试,那你永远不知道自己写的代码有多脆弱。

我记得有一次,一个看似简单的登录页面,手动测试怎么点都没问题。结果上线后,用户在低端机上输入超长用户名时,整个页面卡死了。为什么?因为UI线程被一个没优化好的正则表达式阻塞了。这种问题,单元测试测不出来,只有UI测试能抓到。嗯,从那以后,我再也不敢轻视UI测试了。

Espresso是什么?

Espresso是Google官方推出的Android UI测试框架。说白了,它就是一个能帮你自动操作UI组件、并验证UI状态的工具。你写一段代码,它就能模拟用户点击、输入、滑动等操作,然后检查界面是不是符合预期。

它的核心优势有三点:

  • 同步机制:自动等待UI线程空闲,你不用手动加sleep
  • 流畅的API:链式调用,代码读起来像自然语言
  • 精准匹配:即使界面有多个相似控件,也能精确找到目标

我个人习惯把Espresso比作「UI测试界的JUnit」——简单、专注、高效。

Espresso三大核心组件

要理解Espresso,你只需要记住三个东西:ViewMatchersViewActionsViewAssertions。它们的关系可以用一句话概括:找到谁 → 做什么 → 验证什么

核心公式: onView(withId(R.id.button)).perform(click()).check(matches(isDisplayed()))

翻译成人话:找到那个按钮 → 点击它 → 检查它是不是显示出来了

下面我逐个拆解。

1. ViewMatchers(匹配器)

匹配器的作用是「找到你要操作的控件」。Espresso提供了丰富的匹配器,常用的有:

  • withId(R.id.my_button) — 通过ID匹配
  • withText("登录") — 通过文本匹配
  • withContentDescription("描述") — 通过无障碍描述匹配
  • isDisplayed() — 匹配当前可见的控件
  • isEnabled() — 匹配可交互的控件

我在项目中遇到过一个问题:一个页面有两个TextView,文本都是「确认」。用withText("确认")会匹配到两个,导致测试报错。这时候就需要组合匹配器了,后面会讲。

2. ViewActions(操作)

找到控件后,你要对它做什么?ViewActions就是干这个的:

  • click() — 点击
  • typeText("hello") — 输入文本
  • clearText() — 清空输入框
  • scrollTo() — 滚动到目标控件
  • pressBack() — 按下返回键

小技巧: 使用typeText()时,它会先自动聚焦再输入。如果你不想清空原有内容,可以用replaceText()。我个人习惯用typeText(),因为它更接近真实用户行为。

3. ViewAssertions(断言)

操作完了,你得验证结果对不对。ViewAssertions就是用来做这件事的:

  • matches(isDisplayed()) — 检查控件是否显示
  • matches(withText("成功")) — 检查文本是否匹配
  • matches(isNotChecked()) — 检查复选框是否未选中
  • doesNotExist() — 检查控件是否不存在

你想想看,如果没有断言,你只是点了一通按钮,谁知道结果对不对?断言就是UI测试的「裁判」。

onView与onData的使用

这两个是Espresso的入口方法,但使用场景完全不同。

onView

用于匹配当前布局中可见的控件。绝大多数场景都用它:

onView(withId(R.id.login_button))
    .perform(click())
    .check(matches(isDisplayed()))

注意:如果目标控件在屏幕外(比如在ScrollView底部),你需要先scrollTo()

onView(withText("提交"))
    .perform(scrollTo(), click())

onData

用于匹配AdapterView(如ListView、GridView、Spinner)中的数据项。为什么需要它?因为这些控件的数据是动态加载的,控件本身可能不在视图树中。

我曾经被这个问题坑过:用onView去匹配ListView中的某一项,死活找不到。后来才意识到,ListView的item是复用的,不在屏幕上的item根本不在视图树里。这时候就得用onData

onData(allOf(
    is(instanceOf(String::class.java)),
    is("目标项")
)).inAdapterView(withId(R.id.my_list))
  .perform(click())

注意: onData的匹配逻辑和onView不同。它匹配的是数据对象,而不是视图。如果你传入的是自定义对象,记得实现toString()或使用自定义匹配器。

匹配器组合

单个匹配器有时候不够用。比如前面说的,页面上有两个「确认」按钮。这时候就需要组合匹配器:

onView(allOf(
    withText("确认"),
    withParent(withId(R.id.dialog_layout))
)).perform(click())

常用的组合方式有:

  • allOf() — 所有条件都满足(AND)
  • anyOf() — 满足任一条件(OR)
  • not() — 取反

举个例子,我想匹配「不是禁用的提交按钮」:

onView(allOf(
    withId(R.id.submit_button),
    isEnabled()
)).perform(click())

你想想看,这种组合能力让测试代码变得非常灵活。我见过一些团队,为了匹配一个控件,硬是给每个控件加上了独一无二的contentDescription。其实没必要,组合匹配器就能搞定。

基本交互测试实战

光说不练假把式。我们来看几个最常见的交互场景。

点击测试

@Test
fun testButtonClick() {
    onView(withId(R.id.click_button))
        .perform(click())
    
    onView(withId(R.id.result_text))
        .check(matches(withText("按钮被点击了")))
}

输入测试

@Test
fun testTextInput() {
    onView(withId(R.id.input_field))
        .perform(typeText("Hello Espresso"))
    
    onView(withId(R.id.input_field))
        .check(matches(withText("Hello Espresso")))
}

避坑指南: 我曾经在输入测试中踩过一个坑——输入法弹出来把按钮挡住了。解决方案是在测试前关闭动画和输入法:在测试类的@Before方法中调用Espresso.closeSoftKeyboard()

滚动测试

@Test
fun testScrollToElement() {
    // 先滚动到目标位置
    onView(withId(R.id.scroll_view))
        .perform(scrollTo())
    
    // 再操作目标控件
    onView(withText("页面底部的按钮"))
        .perform(click())
    
    // 验证结果
    onView(withId(R.id.toast_message))
        .check(matches(isDisplayed()))
}

这里要注意:scrollTo()只能用在ScrollViewRecyclerView等可滚动容器中。如果你用的是NestedScrollView,可能需要先找到内部的ScrollView再执行滚动。

知识体系总览

为了让你对本章内容有个整体印象,我画了一张图:

Espresso UI测试知识体系 核心公式:找到谁 → 做什么 → 验证什么 ViewMatchers ViewActions ViewAssertions onView() — 可见控件 onData() — AdapterView数据 匹配器组合:allOf / anyOf / not 点击 输入 滚动

这张图把本章的核心内容串起来了。你从顶部开始,先理解核心公式,然后掌握三大组件,再学会两个入口方法,最后用匹配器组合和基本交互来写测试用例。按这个路径学,不会乱。

写在最后

UI测试入门其实不难,难的是坚持写。我见过太多项目,一开始热情高涨写了几个UI测试,后来因为维护成本高就废弃了。我的建议是:从核心流程开始写,比如登录、注册、支付这些用户必经的路径。先把这些写稳了,再慢慢覆盖边缘场景。

下一章我们会深入RecyclerView的测试,那才是UI测试的真正战场。不过先把今天的基础打牢,后面才能飞起来。


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