第2章:基本UI测试 - 从零开始搭建你的第一个测试
讲真,很多新手一上来就扎进自定义匹配器的深坑,结果连基本的UI测试都没跑通过。我个人习惯是先把地基打牢。这一章,我们来聊聊最基本的UI测试怎么写,怎么跑,以及背后那些你可能忽略的细节。
2.1 测试环境准备
别急着写代码。先把环境配好,否则编译报错能让你怀疑人生。
第一步:添加依赖
在你的 app 模块的 build.gradle 文件里,加上这些依赖。注意版本号要跟你的 Android 版本匹配。
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation写成了implementation。结果测试代码打进了Release包,线上出了大问题。记住,测试依赖一定要用androidTestImplementation,它只在测试APK中生效。
第二步:配置测试运行器
在 defaultConfig 块里指定测试运行器:
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
这一步很多人会忘。你想想看,没有指挥,你的测试代码怎么知道由谁来执行?
2.2 测试文件的结构
我会把测试文件放在 app/src/androidTest/java/ 目录下。这个目录天生就是写UI测试的地方,跟单元测试的 test 目录不一样。
一个基本的测试类长这样:
@RunWith(AndroidJUnit4.class)
public class LoginScreenTest {
@Rule
public ActivityTestRule<LoginActivity> activityRule =
new ActivityTestRule<>(LoginActivity.class);
@Test
public void testLoginButtonDisplayed() {
onView(withId(R.id.login_button))
.check(matches(isDisplayed()));
}
}
这里有几个关键点:
@RunWith(AndroidJUnit4.class):告诉JUnit用Android的测试器来跑。@Rule:ActivityTestRule会在每个测试用例之前启动目标Activity,并在测试结束后关掉它。onView():这是Espresso的入口。你传进去一个ViewMatcher,它返回一个ViewInteraction对象。check():对找到的View执行断言。
2.3 常见的匹配器
写测试说白了就是「找到那个View」。Espresso提供了一堆现成的匹配器,我挑几个最常用的:
| 匹配器 | 作用 | 示例 |
|---|---|---|
withId() |
根据资源ID匹配 | onView(withId(R.id.username)) |
withText() |
根据文本内容匹配 | onView(withText("登录")) |
isDisplayed() |
验证View是否可见 | check(matches(isDisplayed())) |
hasContentDescription() |
根据无障碍描述匹配 | onView(hasContentDescription("返回")) |
hasSibling() |
根据兄弟节点匹配 | onView(hasSibling(withText("密码"))) |
为什么我特别推荐 hasSibling()?因为有时同一页面有好几个同样ID的View(比如RecyclerView的item),用ID匹配会找到一堆。但通过兄弟节点的文本,就能精确锁定目标。我在一个消息列表页就靠它解决了「点击发送按钮,结果每个item的发送都被点了」的尴尬。
2.4 执行用户操作
找到View之后,你得「模拟用户怎么操作它」。Espresso的操作API都在 ViewActions 这个类里:
// 点击
onView(withId(R.id.login_button)).perform(click());
// 输入文本
onView(withId(R.id.username_field)).perform(typeText("test_user"));
// 清除文本
onView(withId(R.id.username_field)).perform(clearText());
// 滑动
onView(withId(R.id.scroll_view)).perform(swipeUp());
// 长按
onView(withId(R.id.item)).perform(longClick());
typeText() 会逐个字符输入,非常慢。如果你要输入大量文本,用 replaceText() 直接替换,速度快得多。不过要注意,
replaceText() 不会触发输入法的某些事件,所以如果你依赖输入法的自动补全,那还是用 typeText() 吧。
2.5 断言与验证
操作完了,得检查结果对不对。Espresso的断言主要靠 ViewAssertions:
// 检查View是否显示
onView(withId(R.id.result_text))
.check(matches(isDisplayed()));
// 检查文本内容
onView(withId(R.id.result_text))
.check(matches(withText("登录成功")));
// 检查View是否不可点击
onView(withId(R.id.submit_button))
.check(matches(not(isClickable())));
// 检查View是否不存在
onView(withText("错误提示"))
.check(doesNotExist());
注意最后一个 doesNotExist(),它用来验证某个View「确实没有出现在界面上」。这在检查错误消失或者动画结束后的状态时特别有用。有一次我测一个加载动画,动画结束后加载框应该消失,但用 isDisplayed() 结合 not() 居然报错了——原因是加载框还在布局里,只是不可见。用 doesNotExist() 才真正验证了它被从布局中移除了。
2.6 组合操作:一个完整的测试用例
好了,把上面这些知识点串起来,写一个完整的登录功能测试:
@Test
public void testLoginSuccess() {
// 1. 找到输入框,输入用户名
onView(withId(R.id.username_field)).perform(typeText("admin"));
// 2. 找到密码框,输入密码
onView(withId(R.id.password_field)).perform(typeText("123456"));
// 3. 关闭软键盘(这一步经常被忽略)
closeSoftKeyboard();
// 4. 点击登录按钮
onView(withId(R.id.login_button)).perform(click());
// 5. 验证跳转到主页
onView(withId(R.id.main_screen))
.check(matches(isDisplayed()));
// 6. 验证用户昵称是否正确
onView(withId(R.id.nickname))
.check(matches(withText("欢迎回来, admin")));
}
这里有个小细节:我在输入密码后加了一行 closeSoftKeyboard()。为什么要这么做?因为软键盘弹出来的时候,可能会挡住登录按钮。Espresso找不到按钮就会报错。我刚开始写测试时几乎每跑必挂,折腾半天才发现是键盘挡了路。
2.7 测试执行与调试
写好了测试,怎么跑?两种方式:
- 命令行:
./gradlew connectedAndroidTest - Android Studio: 直接在测试类或方法上右键 → Run
跑测试的时候,我建议你盯着手机或模拟器的屏幕看。Espresso默认会以肉眼可见的速度操作。如果测试挂了,错误信息会告诉你「找View超时」或者「找到多个View」。这时候照着错误信息去检查布局,基本都能定位问题。
- 没关闭软键盘,导致点击被键盘挡住
- 界面有动画正在播放(比如ViewPager的页面切换),而你在动画结束前就去找View
- 异步操作(比如网络请求)没完成,结果还没渲染到界面上
- RecyclerView的item没有被加载到可见区域
2.8 知识结构总览
说实话,上面讲了这么多,你可能有点乱。我画了一张图,帮你把本章的核心知识点串起来。看一眼就明白基本UI测试的完整流程了。
看明白了没?流程就是从环境搭建到编写测试,再到执行验证。写测试的核心其实就是三个动作:找到元素、模拟操作、检查结果。说白了,跟手工测试的区别就是「把操作步骤写成代码」。这一章的内容够你写出百分之八十的常规测试了。接下来,我们会深入自定义匹配器的世界,让测试代码变得更加灵活简洁。