Espresso是Google为Android平台量身打造的UI自动化测试框架,它最大的特点是运行在被测应用的进程内部,能够感知应用的主线程和空闲状态,从而在合适的时机自动执行操作和断言。这种自动同步机制让测试用例不再需要手写复杂的等待逻辑,稳定性和执行速度都比传统的跨进程自动化工具高出不少。本文将围绕Espresso测试用例的编写展开,从环境配置到核心API的使用,再到常见进阶场景的处理,逐一给出可运行的代码示例。

一、搭建Espresso测试环境
要在项目中使用Espresso,首先需要在模块的build.gradle中引入相关依赖。Espresso通过AndroidX Test库提供,依赖配置如下:
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test:runner:1.5.2'
}
配置完成后,还需要在defaultConfig中通过testInstrumentationRunner指定测试运行器,否则测试用例将无法正常启动:
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
环境就绪后,编写一个最基础的测试类来验证框架是否工作正常。测试类需要使用@RunWith(AndroidJUnit4.class)注解标记,这样JUnit4才会使用Android的测试运行器来执行用例。测试代码放在androidTest源集目录下,而不是unit test目录,这一点新手经常搞混。androidTest目录下的用例会运行在真机或模拟器上,能够真实地操作界面元素。
二、核心三要素:ViewMatcher、ViewAction与ViewAssertion
Espresso的所有测试用例基本都遵循同一个套路:找到某个控件,对它执行某个操作,然后验证某个结果。这三个步骤分别对应三个核心组件,官方称为Espresso三部曲。
第一步是定位控件,使用onView()配合ViewMatcher完成,最常用的是通过控件ID查找。第二步是执行操作,调用.perform()并传入ViewAction,比如click()点击、typeText()输入文字。第三步是验证结果,调用.check()配合ViewAssertion断言控件的状态或内容。下面是一个完整的登录页测试示例:
@Test
public void testLoginSuccess() {
// 在用户名输入框中输入文本,并关闭软键盘避免遮挡
onView(withId(R.id.et_username))
.perform(typeText("test_user"), closeSoftKeyboard());
// 输入密码
onView(withId(R.id.et_password))
.perform(typeText("123456"), closeSoftKeyboard());
// 点击登录按钮
onView(withId(R.id.btn_login)).perform(click());
// 验证登录成功后显示了欢迎提示
onView(withText("登录成功"))
.check(matches(isDisplayed()));
}
除了通过ID定位,Espresso还提供了丰富的匹配器。例如withText()按显示文本查找,withContentDescription()按无障碍描述查找,withHint()按提示语查找。如果单个条件不够精确,可以用allOf()组合多个匹配器。当一个界面中有多个相同ID的控件时(比如RecyclerView的列表项),组合匹配就非常必要:
// 同时满足"有文本OK"且"可见"两个条件的按钮
onView(allOf(withText("确定"), isDisplayed()))
.perform(click());
// 匹配不属于某个父容器的控件
onView(allOf(withId(R.id.btn_delete), not(isDescendantOfA(withId(R.id.header)))))
.check(matches(isEnabled()));
需要注意的是,如果匹配器找到了多个控件,Espresso会直接抛出AmbiguousViewMatcherException异常,测试失败。这不是框架的缺陷,而是在提醒你定位条件不够唯一,应继续补充匹配条件。反之,如果一个控件都没找到,Espresso默认会等待主线程空闲后再报错,这正是它自动同步机制的体现。
三、RecyclerView列表与多控件场景的处理
普通的onView()无法直接定位RecyclerView中的列表项,因为列表项是动态复用的。此时需要使用onViewWithTag()或者Espresso提供的RecyclerViewActions。后者位于espresso-contrib依赖中,专门用于处理列表操作,例如滚动到指定位置、对某一项执行点击:
// 滚动到第5个列表项并点击它
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.actionOnItemAtPosition(4, click()));
// 滚动到匹配某个文本的列表项,然后点击其中的删除按钮
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollToHolder(
viewHolderMatcher(withItemText("订单A")))
.atPositionOnView(R.id.btn_delete, click()));
如果不想引入contrib库,另一个轻量方案是给列表项中的控件设置Tag,然后通过onViewWithTag()定位。这种方式实现简单,但要求在业务代码里为控件额外设置tag属性,对代码有一定侵入性,团队可以根据实际情况权衡。
对于列表数量、状态等更复杂的验证,可以使用onData()配合AdapterView(如ListView、Spinner)使用,它会先把数据加载进来再做匹配。而RecyclerView由于不是传统的AdapterView,仍推荐使用上面的方式处理。
四、Intent验证与Activity规则配置
测试页面跳转时,Espresso Intents模块非常好用。它可以在测试期间拦截真实的Intent,既能验证Intent携带的参数是否正确,又不会真的启动下一个页面,从而保持测试的隔离性:
@RunWith(AndroidJUnit4.class)
public class DetailIntentTest {
@Rule
public IntentsTestRule<MainActivity> activityRule =
new IntentsTestRule<>(MainActivity.class);
@Test
public void testNavigateToDetail() {
onView(withId(R.id.btn_detail)).perform(click());
// 验证跳转Intent的目标组件和数据
intended(allOf(
hasComponent(DetailActivity.class.getName()),
hasExtra("itemId", "1001")));
}
}
如果只需要启动被测Activity,用ActivityScenarioRule即可,这是目前官方推荐的方式,旧的ActivityTestRule已被标记为过时。通过ActivityScenarioRule还能在测试前后对Activity执行各种生命周期操作,比如旋转屏幕验证数据是否丢失:
@Rule
public ActivityScenarioRule<MainActivity> rule =
new ActivityScenarioRule<>(MainActivity.class);
@Test
public void testSurviveRotation() {
onView(withId(R.id.et_note))
.perform(typeText("测试内容"), closeSoftKeyboard());
// 模拟屏幕旋转
rule.getScenario().onActivity(activity ->
activity.setRequestedOrientation(
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE));
// 旋转后内容应该还在
onView(withId(R.id.et_note))
.check(matches(withText("测试内容")));
}
五、提升用例稳定性的实用技巧
编写Espresso用例时,最常见的失败原因是界面尚未就绪就执行了断言,或者输入时软键盘遮挡了目标控件。针对这些问题,有几个经过实践验证的处理办法。
首先是软键盘问题,在每次typeText()之后追加closeSoftKeyboard()几乎应该成为固定习惯。其次是自定义IdlingResource,当被测界面存在自定义的异步加载(比如等待某个动画结束或某个网络回包)时,主线程可能已经空闲但数据还没准备好,此时需要手动注册IdlingResource告诉Espresso当前正处于忙碌状态:
public class NetworkIdlingResource implements IdlingResource {
private volatile boolean isIdle = true;
private ResourceCallback callback;
@Override
public String getName() {
return "NetworkIdlingResource";
}
@Override
public boolean isIdleNow() {
return isIdle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
// 请求开始时调用
public void setLoading() {
isIdle = false;
}
// 请求结束时调用
public void setDone() {
isIdle = true;
if (callback != null) {
callback.onTransitionToIdle();
}
}
}
// 测试中注册
@Test
public void testLoadData() {
Espresso.registerIdlingResources(idlingResource);
onView(withId(R.id.tv_content))
.check(matches(withText("数据加载完成")));
Espresso.unregisterIdlingResources(idlingResource);
}
最后是命名和结构上的建议。测试方法名应清晰表达测试意图,例如testLoginWithEmptyPasswordShowsError,一眼就能看出验证的是什么场景。每个用例只关注一个行为点,测试之间不要相互依赖,断言要具体到控件的可见性或内容,避免使用Thread.sleep()这种硬等待。遵循这些原则,配合Espresso本身的自动同步机制,用例的稳定性会有明显提升,长期维护成本也会大幅降低。
EspressoAndroid自动化测试UI测试修改时间:2026-09-12 03:26:37