UI测试基础与Espresso入门
说实话,很多Android开发者对UI测试的态度是「知道很重要,但就是不想写」。我完全理解——写UI测试确实比写单元测试麻烦,要配环境、要等模拟器启动、还要处理各种异步问题。但我要说一句:如果你从来没写过UI测试,那你永远不知道自己写的代码有多脆弱。
我记得有一次,一个看似简单的登录页面,手动测试怎么点都没问题。结果上线后,用户在低端机上输入超长用户名时,整个页面卡死了。为什么?因为UI线程被一个没优化好的正则表达式阻塞了。这种问题,单元测试测不出来,只有UI测试能抓到。嗯,从那以后,我再也不敢轻视UI测试了。
Espresso是什么?
Espresso是Google官方推出的Android UI测试框架。说白了,它就是一个能帮你自动操作UI组件、并验证UI状态的工具。你写一段代码,它就能模拟用户点击、输入、滑动等操作,然后检查界面是不是符合预期。
它的核心优势有三点:
- 同步机制:自动等待UI线程空闲,你不用手动加sleep
- 流畅的API:链式调用,代码读起来像自然语言
- 精准匹配:即使界面有多个相似控件,也能精确找到目标
我个人习惯把Espresso比作「UI测试界的JUnit」——简单、专注、高效。
Espresso三大核心组件
要理解Espresso,你只需要记住三个东西:ViewMatchers、ViewActions、ViewAssertions。它们的关系可以用一句话概括:找到谁 → 做什么 → 验证什么。
核心公式: 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()只能用在ScrollView或RecyclerView等可滚动容器中。如果你用的是NestedScrollView,可能需要先找到内部的ScrollView再执行滚动。
知识体系总览
为了让你对本章内容有个整体印象,我画了一张图:
这张图把本章的核心内容串起来了。你从顶部开始,先理解核心公式,然后掌握三大组件,再学会两个入口方法,最后用匹配器组合和基本交互来写测试用例。按这个路径学,不会乱。
写在最后
UI测试入门其实不难,难的是坚持写。我见过太多项目,一开始热情高涨写了几个UI测试,后来因为维护成本高就废弃了。我的建议是:从核心流程开始写,比如登录、注册、支付这些用户必经的路径。先把这些写稳了,再慢慢覆盖边缘场景。
下一章我们会深入RecyclerView的测试,那才是UI测试的真正战场。不过先把今天的基础打牢,后面才能飞起来。
公众号:蓝海资料掘金营,微信deep3321