第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执行断言。
核心思想: Espresso的API遵循「找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测试的完整流程了。

基本UI测试流程 环境准备 编写测试 执行测试 匹配器 (Matcher) 操作 (Action) 断言 (Assertion) 常见匹配器 • withId • withText • hasSibling 常用操作 • click • typeText • swipeUp 常用断言 • isDisplayed • matches(text) • doesNotExist 组合应用:找View → 做操作 → 验证结果 闭包软键盘、等待动画结束、注意异步操作 —— 测试失败的三大元凶

看明白了没?流程就是从环境搭建到编写测试,再到执行验证。写测试的核心其实就是三个动作:找到元素、模拟操作、检查结果。说白了,跟手工测试的区别就是「把操作步骤写成代码」。这一章的内容够你写出百分之八十的常规测试了。接下来,我们会深入自定义匹配器的世界,让测试代码变得更加灵活简洁。