Android 自动化测试完全指南(新人版)
写给谁看 :刚入门的 Android 开发,即使从未写过测试,也能按本文一步步在任意项目里手写自动化测试。
本文目标 :讲清「每种方案是什么、怎么引入、怎么手写、怎么跑、怎么选」。
示例约定 :文中代码全部使用虚构的演示包名
com.example.demoapp,不依赖任何真实业务工程,可直接照着改到你自己的项目。
目录
- 先建立正确认知
- 一张表:怎么选方案
- [Android 项目里和测试有关的目录](#Android 项目里和测试有关的目录 "#3-android-%E9%A1%B9%E7%9B%AE%E9%87%8C%E5%92%8C%E6%B5%8B%E8%AF%95%E6%9C%89%E5%85%B3%E7%9A%84%E7%9B%AE%E5%BD%95")
- [方案 A:JUnit 本地单元测试](#方案 A:JUnit 本地单元测试 "#4-%E6%96%B9%E6%A1%88-ajunit-%E6%9C%AC%E5%9C%B0%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95")
- [方案 B:Robolectric(JVM 里模拟 Android)](#方案 B:Robolectric(JVM 里模拟 Android) "#5-%E6%96%B9%E6%A1%88-brobolectricjvm-%E9%87%8C%E6%A8%A1%E6%8B%9F-android")
- [方案 C:Espresso(View 页面像人一样点)](#方案 C:Espresso(View 页面像人一样点) "#6-%E6%96%B9%E6%A1%88-cespressoview-%E9%A1%B5%E9%9D%A2%E5%83%8F%E4%BA%BA%E4%B8%80%E6%A0%B7%E7%82%B9")
- [方案 D:Compose UI Test](#方案 D:Compose UI Test "#7-%E6%96%B9%E6%A1%88-dcompose-ui-test")
- [方案 E:UI Automator(跨应用 / 系统弹窗)](#方案 E:UI Automator(跨应用 / 系统弹窗) "#8-%E6%96%B9%E6%A1%88-eui-automator%E8%B7%A8%E5%BA%94%E7%94%A8--%E7%B3%BB%E7%BB%9F%E5%BC%B9%E7%AA%97")
- [方案 F:Maestro(YAML 业务剧本)](#方案 F:Maestro(YAML 业务剧本) "#9-%E6%96%B9%E6%A1%88-fmaestroyaml-%E4%B8%9A%E5%8A%A1%E5%89%A7%E6%9C%AC")
- [方案 G:Appium(跨端黑盒)](#方案 G:Appium(跨端黑盒) "#10-%E6%96%B9%E6%A1%88-gappium%E8%B7%A8%E7%AB%AF%E9%BB%91%E7%9B%92")
- [方案 H:Firebase Test Lab / Robo / Robo Script](#方案 H:Firebase Test Lab / Robo / Robo Script "#11-%E6%96%B9%E6%A1%88-hfirebase-test-lab--robo--robo-script")
- [方案 I:Gradle Managed Devices(多 Android 版本矩阵)](#方案 I:Gradle Managed Devices(多 Android 版本矩阵) "#12-%E6%96%B9%E6%A1%88-igradle-managed-devices%E5%A4%9A-android-%E7%89%88%E6%9C%AC%E7%9F%A9%E9%98%B5")
- 新人推荐学习路线
- [常见问题 FAQ](#常见问题 FAQ "#14-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-faq")
- 官方文档入口
1. 先建立正确认知
1.1 自动化测试在干什么?
把原来「人打开 App → 点按钮 → 看结果」的过程,写成脚本,让电脑在:
- 电脑本机 JVM(不需要手机),或
- 模拟器,或
- 真机 / 云端手机
上自动执行,并告诉你:通过 还是失败。
1.2 两类完全不同的「自动点」
| 类型 | 像什么 | 是否按业务逻辑 |
|---|---|---|
| 乱点探索(Monkey、默认 Robo) | 猴子随便点 | 否 |
| 按剧本点(Espresso / Maestro / Appium 等) | 测试员按用例操作 | 是 |
你若想「登录 → 进首页 → 点预约 → 填表 → 断言成功」,选的是 按剧本点,不是乱点。
1.3 测试金字塔(记这个就够)
bash
/\
/ \ E2E / 跨应用 / 云真机(少、慢、贵)
/----\
/ UI \ Espresso / Compose / Maestro(中)
/--------\
/ 单元测试 \ JUnit / Robolectric(多、快、便宜)
/______________\
原则:底层多写、上层少写。不要一上来就只写又慢又脆的整 App 流程。
1.4 三个关键名词
| 名词 | 中文理解 |
|---|---|
| Unit Test / 本地测试 | 在电脑上跑,目录一般是 src/test/,通常不需要模拟器 |
| Instrumented Test / 仪器化测试 | 安装到模拟器或真机上跑,目录一般是 src/androidTest/ |
| 断言(Assertion) | 检查「实际结果是不是等于期望」;不对就让测试失败 |
2. 一张表:怎么选方案
按你的目标从上往下找:
| 我想做什么 | 优先选 | 备选 |
|---|---|---|
| 测工具类、计算、字符串、ViewModel 逻辑 | A JUnit | B Robolectric |
| 想测一点 Android API,但不想开模拟器 | B Robolectric | A + 少量仪器化 |
| 测传统 XML/View 页面:输入、点击、跳转 | C Espresso | F Maestro |
| 测 Jetpack Compose 界面 | D Compose Test | C(混用时) |
| 要处理权限弹窗、通知栏、跨 App | E UI Automator | 与 C 组合 |
| 希望产品/测试用「步骤清单」写主流程 | F Maestro | C Espresso |
| 同一套脚本测 Android + iOS | G Appium | F(能力较弱时) |
| 不想写很多用例,发版前广扫崩溃 | H Robo / Test Lab | 仅作补充 |
| 要按业务路径点,又想少写代码 | H Robo Script 或 F Maestro | C |
| 同一套 UI 测试跑多个 Android 版本(可不靠真机) | I GMD + C/D | H 云测 |
2.1 一句话决策
- 只测逻辑 → JUnit
- 测自己 App 里的页面(View) → Espresso
- 测 Compose 页面 → Compose Test
- 测主流程、想写得简单 → Maestro
- 碰到系统弹窗 / 多 App → UI Automator
- 要 iOS 也一起 → Appium
- 发版前多机型扫一遍 → Firebase Test Lab
- CI 上多 API 版本 → Gradle Managed Devices
2.2 新人最稳妥的默认组合
JUnit(逻辑) + Espresso 或 Compose Test(关键页面) + 可选 Maestro(冒烟)
先把这三样学会,再考虑 Appium / 云测。
3. Android 项目里和测试有关的目录
标准模块(如 app)里通常有:
text
app/
├── src/
│ ├── main/ ← 正式 App 代码(用户安装的)
│ ├── test/ ← 本地单元测试(电脑跑,快)
│ │ └── java/com/example/demoapp/
│ │ └── ExampleUnitTest.java
│ └── androidTest/ ← 仪器化测试(模拟器/真机跑)
│ └── java/com/example/demoapp/
│ └── ExampleInstrumentedTest.java
└── build.gradle ← 在这里加测试依赖
| 目录 | 跑在哪里 | 典型用途 |
|---|---|---|
src/test/ |
电脑 JVM | JUnit、Robolectric |
src/androidTest/ |
设备/模拟器 | Espresso、Compose Test、UI Automator |
怎么跑(Android Studio):
- 打开测试类,点类名或方法左边的绿色三角形 ▶
- 或顶部菜单:
Run→ 选择对应测试 - 或 Terminal:
bash
# 只跑本地单元测试
./gradlew test
# 跑已连接设备/模拟器上的仪器化测试
./gradlew connectedAndroidTest
Windows 可用 gradlew.bat test。
4. 方案 A:JUnit 本地单元测试
4.1 它是什么?
在电脑上 测试纯逻辑:加减、校验手机号、格式化金额、ViewModel 计算等。
不打开 App 界面,所以最快、最稳。
4.2 适合 / 不适合
| 适合 | 不适合 |
|---|---|
| 工具类、算法、数据转换 | 真实点击按钮 |
| ViewModel 业务分支 | 真实网络(除非 Mock) |
| 不依赖 Android 控件的代码 | 必须有 Context/Activity 才跑得通的逻辑(可改用 Robolectric) |
4.3 如何在项目中引入
打开模块的 build.gradle(Groovy)或 build.gradle.kts(Kotlin),在 dependencies 里加入:
groovy
dependencies {
// JUnit4:写测试用例的基础框架
testImplementation "junit:junit:4.13.2"
// (可选)断言库,写起来更清晰
testImplementation "com.google.truth:truth:1.4.2"
// (可选)Mock 框架,用来伪造依赖
testImplementation "org.mockito:mockito-core:5.11.0"
}
每一行什么意思:
testImplementation:只给src/test用,不会打进正式 APKjunit:提供@Test、Assert.assertEquals等truth:可选,写成assertThat(x).isEqualTo(y)更易读mockito:可选,伪造接口返回值
同步 Gradle(Android Studio 提示 Sync Now)。
4.4 被测代码示例(正式代码)
假设你有一个手机号校验工具:
java
// 文件:src/main/java/com/example/demoapp/util/PhoneUtils.java
package com.example.demoapp.util;
public class PhoneUtils {
/** 简单判断:是否为 11 位且以 1 开头的大陆手机号 */
public static boolean isValidMobile(String phone) {
if (phone == null) {
return false;
}
return phone.matches("^1\\d{10}$");
}
}
4.5 手写测试(一步步)
java
// 文件:src/test/java/com/example/demoapp/util/PhoneUtilsTest.java
package com.example.demoapp.util;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* 测试 PhoneUtils。
* 类名习惯:被测类名 + Test
*/
public class PhoneUtilsTest {
@Test // 告诉 JUnit:这是一个可执行的测试方法
public void validPhone_returnsTrue() {
// Arrange(准备):准备输入
String phone = "13800138000";
// Act(执行):调用被测方法
boolean result = PhoneUtils.isValidMobile(phone);
// Assert(断言):检查结果
assertTrue(result);
}
@Test
public void shortPhone_returnsFalse() {
assertFalse(PhoneUtils.isValidMobile("13800"));
}
@Test
public void nullPhone_returnsFalse() {
assertFalse(PhoneUtils.isValidMobile(null));
}
}
注释里的 AAA 模式(强烈建议新人一直用):
- Arrange:准备数据
- Act:调用方法
- Assert:断言结果
4.6 怎么运行
- Android Studio:打开
PhoneUtilsTest,点 ▶ - 命令行:
./gradlew test - 报告一般在:
app/build/reports/tests/test/index.html
4.7 新人检查清单
- 文件放在
src/test/,不是androidTest - 依赖用的是
testImplementation - 方法有
@Test - 至少有一个
assertXxx(没有断言的测试没有意义)
5. 方案 B:Robolectric(JVM 里模拟 Android)
5.1 它是什么?
让单元测试在电脑上也能用一部分 Android API(如 Context、部分资源、甚至部分 UI),不必启动模拟器 。
由 Google 维护的开源框架。
5.2 适合 / 不适合
| 适合 | 不适合 |
|---|---|
| 需要 Context 的工具类 | WebView、复杂硬件 |
| 想快速跑一批「轻量 UI 行为」 | 系统级 UI、真实相机 |
| CI 里不想每次起模拟器 | 需要 100% 真机保真度时 |
官方说明:Robolectric 支持较广的 API,但不是完整系统,不能完全替代设备测试。
5.3 如何引入
groovy
dependencies {
testImplementation "junit:junit:4.13.2"
// Robolectric:在 JVM 中模拟 Android 环境
testImplementation "org.robolectric:robolectric:4.13"
}
在 android { ... } 里可加(按 AGP 版本略有差异):
groovy
android {
testOptions {
unitTests {
includeAndroidResources = true // 允许测试读到 res 资源
}
}
}
5.4 手写示例
java
// 文件:src/test/java/com/example/demoapp/ExampleRobolectricTest.java
package com.example.demoapp;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(RobolectricTestRunner.class) // 用 Robolectric 的测试运行器
@Config(sdk = 34) // 指定模拟的 Android API 级别
public class ExampleRobolectricTest {
@Test
public void appContext_isNotNull() {
// ApplicationProvider 能在 Robolectric 环境下拿到 Context
Context context = ApplicationProvider.getApplicationContext();
assertNotNull(context);
assertEquals("com.example.demoapp", context.getPackageName());
}
}
每句什么意思:
@RunWith(RobolectricTestRunner.class):不要用默认 Runner,改用 Robolectric@Config(sdk = 34):假装跑在 Android 14(API 34)上;可改成 28、31 等做兼容抽查ApplicationProvider.getApplicationContext():拿到应用 Context
5.5 多版本怎么测?
可以写多个方法,或用参数化;简单做法:
java
@Config(sdk = 28)
@Test
public void worksOnApi28() { /* ... */ }
@Config(sdk = 34)
@Test
public void worksOnApi34() { /* ... */ }
5.6 怎么运行
与 JUnit 相同:./gradlew test 或点 ▶。
仍然在 src/test/,不开模拟器。
6. 方案 C:Espresso(View 页面像人一样点)
6.1 它是什么?
Google 官方的 App 内 UI 自动化 框架。
专门测 XML / View 体系界面:找到控件 → 输入/点击 → 断言文字或是否显示。
这就是你要的:按业务逻辑自动点,而不是乱点。
6.2 适合 / 不适合
| 适合 | 不适合 |
|---|---|
| 登录、表单、列表点击、页内跳转 | 完全黑盒且测试员不会写代码时(可看 Maestro) |
| 开发自测、回归关键页 | 系统权限框(需配合 UI Automator) |
| 只要测「自己这个 App」内部 | Compose 为主的界面(优先 Compose Test) |
6.3 如何引入
groovy
android {
defaultConfig {
// 仪器化测试运行器(一般 AGP 新建工程已有)
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
// AndroidX Test 核心
androidTestImplementation "androidx.test.ext:junit:1.2.1"
androidTestImplementation "androidx.test:runner:1.6.2"
androidTestImplementation "androidx.test:rules:1.6.1"
// Espresso 核心:onView / click / typeText
androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1"
// (可选)测 Intent 跳转
androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1"
// (可选)RecyclerView 等
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.6.1"
}
关键词解释:
androidTestImplementation:只给src/androidTest用testInstrumentationRunner:在设备上启动测试的「管家」espresso-core:最常用的点击、输入、断言
6.4 被测页面示例(正式代码)
java
// 简化版登录页:两个输入框 + 一个按钮
// activity_login.xml 里有:
// R.id.edit_phone、R.id.edit_password、R.id.btn_login、R.id.tv_error
假设点击登录后,手机号为空会显示错误文案「请输入手机号」。
6.5 手写 Espresso 测试(完整模板)
java
// 文件:src/androidTest/java/com/example/demoapp/LoginEspressoTest.java
package com.example.demoapp;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class) // 在设备上跑 Android 仪器化测试
@LargeTest // 标记:这是较重的 UI 测试(可选)
public class LoginEspressoTest {
/**
* ActivityScenarioRule:
* 每个 @Test 开始前自动启动 LoginActivity,
* 结束后自动关闭。新人优先用这个,少写样板代码。
*/
@Rule
public ActivityScenarioRule<LoginActivity> activityRule =
new ActivityScenarioRule<>(LoginActivity.class);
@Test
public void emptyPhone_showsError() {
// 1. 找到手机号输入框,清空并关掉键盘
onView(withId(R.id.edit_phone))
.perform(clearText(), closeSoftKeyboard());
// 2. 密码随便填一点(模拟真实用户)
onView(withId(R.id.edit_password))
.perform(typeText("123456"), closeSoftKeyboard());
// 3. 点击登录按钮(像人点一下)
onView(withId(R.id.btn_login)).perform(click());
// 4. 断言:错误提示出现,且文案正确
onView(withId(R.id.tv_error))
.check(matches(isDisplayed()));
onView(withId(R.id.tv_error))
.check(matches(withText("请输入手机号")));
}
@Test
public void loginButton_isVisible() {
onView(withId(R.id.btn_login)).check(matches(isDisplayed()));
onView(withId(R.id.btn_login)).check(matches(withText("登录")));
}
}
6.6 Espresso 三板斧(务必背熟)
所有 Espresso 几乎都是这三步:
text
onView( 匹配器 Matcher ) → 找到哪个控件
.perform( 动作 Action ) → 做什么(点、输入)
.check( 断言 Assertion ) → 检查变成什么样
| 常用 Matcher | 含义 |
|---|---|
withId(R.id.xxx) |
按 id 找(最推荐) |
withText("登录") |
按文字找 |
isDisplayed() |
是否可见 |
| 常用 Action | 含义 |
|---|---|
click() |
点击 |
typeText("abc") |
输入文字 |
clearText() |
清空 |
closeSoftKeyboard() |
关键盘(常能减少 flaky) |
| 常用 Assertion | 含义 |
|---|---|
matches(isDisplayed()) |
控件显示中 |
matches(withText("xx")) |
文字等于 xx |
6.7 业务路径示例(像人工点完整条流程)
java
@Test
public void loginSuccess_thenOpenHomeTab() {
// 输入正确账号密码(测试环境账号,不要用生产真实用户密码写死到仓库)
onView(withId(R.id.edit_phone))
.perform(typeText("13800138000"), closeSoftKeyboard());
onView(withId(R.id.edit_password))
.perform(typeText("password"), closeSoftKeyboard());
onView(withId(R.id.btn_login)).perform(click());
// 断言已经到首页:例如出现「首页」标题
onView(withText("首页")).check(matches(isDisplayed()));
// 再点「我的」Tab
onView(withId(R.id.tab_mine)).perform(click());
onView(withText("设置")).check(matches(isDisplayed()));
}
这就是「有目的的自动点击 + 页面跳转」。
6.8 怎么运行
- 先开一个模拟器,或插上真机并打开 USB 调试
- 运行:
./gradlew connectedDebugAndroidTest - 或只跑某一个类(Android Studio 里点 ▶ 最简单)
6.9 新人常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 找不到 View | id 错了 / 还在别的 Fragment | 用 Layout Inspector 确认 id |
| 偶发失败 | 动画、网络、键盘 | closeSoftKeyboard;关动画;IdlingResource |
| 一点击就跳走测不到 | Activity 被别的逻辑自动登录带走 | 测试前清登录态 / 用测试入口 |
| 同一 id 匹配到多个 | Fragment 叠层 / 标题栏重复 | 加 withText、只点可见的、或自定义 Matcher |
| 控件「有」但点不到 | gone、宽高为 0、Pad 大边距在手机模拟器上 |
换断言目标;withEffectiveVisibility;必要时 force 点击 |
6.10 为啥 Espresso「默认不等网络返回」?
Espresso 默认只同步主线程消息队列(以及已注册的 IdlingResource),保证的是:
「UI 线程这会儿看起来空了」
而不是:
「接口已经回来了」
典型请求路径是:主线程发起 OkHttp → 后台线程 收响应 → 再 post 回主线程改 UI。
后台那一步 Espresso 看不见,请求刚发出主线程就可能已 idle,于是立刻点下一步------数据还没到、列表还是空的。
非要等网络也可以 ,正统做法是挂 IdlingResource (或给统一的 OkHttpClient 加 Idling 拦截器:请求中 busy、结束后 idle)。
老项目若网络出口分散、回调路径乱,改动可能偏大;务实折中是 等 UI 结果出现(轮询目标 id / 文案),或短期用 sleep(慢且脆)。
6.11 「冷启」是应用冷启,不是模拟器重启
这里说的冷启通常指:
- App / Activity 冷启动 :每条
@Test用ActivityScenarioRule(或launch)重新拉起页面 - 不是每次把 AVD / 模拟器进程关掉再开
模拟器可以一直开着、已解锁;耗时大头往往是「每条用例重新创建 Activity」。
6.12 为啥每条用例都重启 Activity?几个页面能不能一起测?
ActivityScenarioRule 的默认行为是:每个 @Test 开始启动、结束关闭 。
目的是换 隔离:上一条点进改密、开了弹窗、退出登录,下一条不该带着脏状态;失败也好定位。
| 做法 | 说明 |
|---|---|
| 同一次启动里多步合成一条场景(推荐) | 例如一次进设置,扫完多个子菜单再结束。少冷启,风险可控 |
一条用例里先后 launch 多个 Activity |
可以,仍是按需启停,只是写在同一个 @Test |
| 整个测试类共用一个永不关的 Activity | 最快,但串状态、失败难查、退出登录后全挂;一般不建议当默认 |
破坏性步骤(如 signOut、会 finish 的 pressBack)应 单独成条、放最后,不要和前面的菜单冒烟硬串。
6.13 当初为啥要 sleep?现在怎么等更合适?
早期加固定 sleep,通常是为了 先把冒烟跑稳,而不是故意拖时间:
- 等异步 UI:Fragment 切换、列表刷新、弹窗,Espresso 默认同步不到你们业务异步
- 挡系统/业务弹窗 :权限、「知道了」等会抢焦点;旧写法对每个文案
wait(超时),没弹窗也空等满超时,全量会被拖很慢 - 模拟器更抖:按慢设备把等待拉长,减少偶发失败
- 没有 IdlingResource 时的权宜之计:老工程改造成本高时,sleep / 轮询最省事
更合适的演进:
- 弹窗:瞬时
hasObject,有再点 - 启动后:轻量
settleUi(扫弹窗 + 很短一帧) - 切页后:
waitForRes/ 等目标 id 出现即返回,少整秒级固定 sleep - 真要稳等网络:集中挂 IdlingResource
6.14 写 Espresso 时:风险小、耗时可控的建议
- 少冷启:同页多断言合并成场景测;日常只跑相关类,发版再全量
- 早返回等待:轮询目标控件,避免「没东西也空等」
- 选择器对真实 UI :重复 id、空文案宽 0、
EmptyRecyclerView空态 GONE 等,按真机表现写 - 破坏性操作隔离:退出登录等单独用例
- 环境 :模拟器已开已解锁;
animationsDisabled true;离线冒烟尽量 seed 本地会话,少绑死服务器 - 不要为了省启动让所有用例共享一份可变的全局界面状态
7. 方案 D:Compose UI Test
7.1 它是什么?
专门给 Jetpack Compose 界面用的官方测试库。
用「语义 Semantics」(文字、角色、testTag)找控件,而不是传统 R.id。
若项目还是 XML View 为主,可先跳过本章,学 Espresso。
7.2 如何引入
groovy
dependencies {
androidTestImplementation "androidx.compose.ui:ui-test-junit4:<compose_version>"
debugImplementation "androidx.compose.ui:ui-test-manifest:<compose_version>"
}
<compose_version> 与项目 Compose 版本保持一致。
7.3 手写示例
正式 UI(节选):
kotlin
// 给关键按钮加 testTag,方便测试稳定找到它
Button(
onClick = { /* 登录 */ },
modifier = Modifier.testTag("login_button")
) {
Text("登录")
}
测试:
kotlin
// 文件:src/androidTest/.../LoginComposeTest.kt
@RunWith(AndroidJUnit4::class)
class LoginComposeTest {
// createAndroidComposeRule:启动 Activity,并提供 Compose 测试 API
@get:Rule
val composeRule = createAndroidComposeRule<LoginActivity>()
@Test
fun loginButton_isDisplayed() {
// 通过 testTag 找到按钮
composeRule.onNodeWithTag("login_button").assertIsDisplayed()
// 或通过文字
composeRule.onNodeWithText("登录").assertIsDisplayed()
}
@Test
fun clickLogin_withEmptyPhone_showsError() {
composeRule.onNodeWithTag("login_button").performClick()
composeRule.onNodeWithText("请输入手机号").assertIsDisplayed()
}
}
含义:
onNodeWithTag:按你在代码里写的testTag找节点performClick:点击assertIsDisplayed:断言可见
7.4 View + Compose 混合
官方支持在同一仪器化测试里配合使用;原则是:
- Compose 区域 → Compose Test API
- 传统 View 区域 → Espresso
8. 方案 E:UI Automator(跨应用 / 系统弹窗)
8.1 它是什么?
可以操作本 App 以外 的界面:系统权限框、通知、设置页、其他 App。
Espresso 主要在本进程内;跨边界时用 UI Automator。
可以把它想成:Espresso 像在 App 里点控件 ;UI Automator 像拿着手机在 整台设备上操作。
UI Automator 2.4+ 提供更现代的 Kotlin DSL;下面同时给经典写法(Java 新人更好懂)。
8.1.1 和 Espresso 有何异同?
| Espresso | UI Automator | |
|---|---|---|
| 视野 | 本 App 的 View 树 | 整机可访问界面(多窗口 / 多进程) |
| 典型场景 | 点 Tab、填表单、断言标题 | 关权限弹窗、跨 App、系统键 |
| 定位方式 | withId / withText 等 Matcher |
By.res / By.text 等 |
| 同步 | 等主线程空闲(+ IdlingResource) | 多为显式 wait(Until...) / 超时 |
| 速度与稳定性 | 同 App 内通常更快、更细 | 更「黑盒」,有时更慢、更脆 |
相同点 :都是仪器测试(androidTest),都在真机/模拟器上点真实界面,都能做冒烟。
常见配合 :Espresso 测页内逻辑;碰到系统权限窗(另一进程)时用 UI Automator 点掉,再回到 Espresso。
也可在 Espresso 辅助工具里 借用 UiDevice.hasObject 扫弹窗------那是辅助,不是用 UI Automator 替代整页细测。
8.2 如何引入
groovy
dependencies {
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.3.0"
// 若使用 2.4+ 新 DSL,按官方 release 更新版本号
}
8.3 经典写法示例(处理权限弹窗)
java
// 文件:src/androidTest/.../PermissionDialogTest.java
package com.example.demoapp;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;
import org.junit.Before;
import org.junit.Test;
public class PermissionDialogTest {
private UiDevice device;
@Before
public void setUp() {
// UiDevice = 「整台手机的遥控器」
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}
@Test
public void allowPermissionIfShown() {
// 等待最多 3 秒,看是否出现「允许」按钮
boolean appeared = device.wait(Until.hasObject(By.text("允许")), 3000);
if (appeared) {
device.findObject(By.text("允许")).click();
}
// 没有弹窗也不失败:业务上「有就点掉」
}
}
含义:
UiDevice:代表整机By.text("允许"):按屏幕上的字找控件wait(Until..., timeout):最多等一会儿,避免闪崩
8.4 和 Espresso 怎么配合?
常见模式:
- Espresso 点到会触发权限的按钮
- UI Automator 点系统「允许」
- 再回到 Espresso 继续业务断言
8.5 何时不要用它硬测业务页?
纯 App 内页面优先 Espresso/Compose:更快、同步更好、更少 flaky。
9. 方案 F:Maestro(YAML 业务剧本)
9.1 它是什么?
用 YAML 文件 描述「用户操作步骤」,像写产品验收清单。
非常适合:有目的的主流程冒烟(登录、下单、关键 Tab)。
不写 Java 也能做出「像人点」的效果。
9.2 适合新人吗?
适合。尤其当你想快速固化「主路径」,又不想先学一整套 Espresso API。
9.3 如何安装(本机)
- 安装 Maestro CLI(以官方安装方式为准,常见为):
bash
curl -Ls "https://get.maestro.mobile.dev" | bash
Windows 请查看 Maestro 官方文档 的安装说明(可用包管理器或发布包)。
- 确保已连接模拟器或真机:
adb devices能看到设备。 - 安装并启动你的 App。
9.4 在项目中如何组织文件
建议在仓库建目录(与 androidTest 并列管理即可):
text
maestro/
├── login_smoke.yaml
└── home_tabs.yaml
9.5 手写 YAML(完整示例)
yaml
# 文件:maestro/login_smoke.yaml
# appId:要测的应用包名(改成你的)
appId: com.example.demoapp
---
# 启动 App
- launchApp
# 点击「登录」文字(也可用 id)
- tapOn: "登录"
# 在手机号框输入
- tapOn:
id: "edit_phone" # 若使用 View id,需按 Maestro 文档配置可见性
- inputText: "13800138000"
- tapOn:
id: "edit_password"
- inputText: "password"
- tapOn: "登录"
# 断言:首页关键文案出现(说明跳转成功)
- assertVisible: "首页"
每一行什么意思:
| 指令 | 含义 |
|---|---|
appId |
测哪个包 |
launchApp |
启动应用 |
tapOn |
点击某文字或控件 |
inputText |
输入文本 |
assertVisible |
断言某内容可见 |
9.6 怎么运行
bash
maestro test maestro/login_smoke.yaml
9.7 和 Espresso、UI Automator 怎么选?
| Espresso | UI Automator | Maestro | |
|---|---|---|---|
| 写什么 | Java/Kotlin 测试代码 | Java/Kotlin + UiDevice |
YAML 剧本 |
| 主要目的 | App 内 View 细测 | 系统弹窗 / 跨 App | 整条业务路径 |
| 运行方式 | Gradle instrumented | 同左 | Maestro CLI(maestro test) |
| 同步 | 主线程 + Idling | 显式 wait | 内置等可见 / 超时 |
| 粒度 | 细(校验、表单、边界) | 中(找得到、点得掉) | 粗(流程通不通) |
| 改 UI 后维护 | Matcher / id 要改 | 类似 | YAML 里 id / 文案要改 |
相同点:都在真机/模拟器上点真实界面;都能做冒烟;界面 id/文案变了都要维护。
主责不同(不要互相替代):
| 层 | 回答的问题 |
|---|---|
| Espresso | App 里各个页面控件对不对 |
| UI Automator | 系统弹窗挡路时能不能过 |
| Maestro | 整条业务用户路径还能不能走通 |
建议:核心回归用 Espresso;系统弹窗用 UI Automator(或在辅助里借用);主路径冒烟可同时用 Maestro。
10. 方案 G:Appium(跨端黑盒)
10.1 它是什么?
基于 WebDriver 的跨平台自动化:Android / iOS 都能测。
测试代码可用 Java、Python、JavaScript 等,不一定写在 Android 工程里。
10.2 适合 / 不适合
| 适合 | 不适合 |
|---|---|
| 有专职 QA、要同时覆盖 iOS | 只有 Android、团队全是 App 开发 |
| 复杂黑盒回归 | 想最快上手(学习成本高) |
| 已有 Selenium 经验的团队 | 小型项目(维护成本往往高于收益) |
10.3 使用需要哪些部件(概念)
text
你的测试脚本 → Appium Server → 安卓设备上的驱动 → App
新人需要准备:
- Node.js(常用于安装 Appium)
- Appium Server
- Android SDK / 模拟器或真机
- 一种语言的客户端库(如 Java 的
java-client)
10.4 最小 Java 示例(示意)
java
// 这是独立测试工程里的示意代码,不必放进 app 模块
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("platformName", "Android");
caps.setCapability("appium:automationName", "UiAutomator2");
caps.setCapability("appium:deviceName", "Android Emulator");
caps.setCapability("appium:appPackage", "com.example.demoapp");
caps.setCapability("appium:appActivity", ".LoginActivity");
AndroidDriver driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), caps);
WebElement phone = driver.findElement(AppiumBy.id("com.example.demoapp:id/edit_phone"));
phone.sendKeys("13800138000");
driver.findElement(AppiumBy.id("com.example.demoapp:id/btn_login")).click();
driver.quit();
含义:
capabilities:告诉 Appium「用什么设备、打开哪个 App」findElement+click/sendKeys:找控件并操作quit:结束会话
10.5 新人建议
若你只做 Android Pad / 单端 App:先精通 Espresso,不要急着上 Appium 。
等公司明确要求「安卓 iOS 一套脚本」再学。
11. 方案 H:Firebase Test Lab / Robo / Robo Script
11.1 它是什么?
Google 提供的云端测试农场:把 APK 上传后,在云端的虚拟机或真机上跑测试。
三种常见用法:
| 名称 | 做什么 | 要不要写用例 |
|---|---|---|
| Instrumentation | 跑你写的 Espresso 等 | 要 |
| Robo | 自动探索点击,找崩溃 | 基本不用 |
| Robo Script | 先按你录制的业务路径走,再可选探索 | 录制/JSON |
11.2 Robo:默认是「探索」,不是严格业务剧本
默认 Robo 可能东点西点,适合找崩溃,不适合代替「登录后下单」的验收。
若要业务路径:用 Robo Script。
11.3 Robo Script:录制「像人走一遍」
大致步骤(以 Android Studio + Firebase 为准,菜单名随版本可能微调):
- 安装 Firebase / Test Lab 相关插件(若需要)
- 用 Record Robo Script 功能,在模拟器上亲手走一遍业务
- 得到 JSON 脚本文件
- 上传 APK + 脚本到 Firebase Test Lab
- 云端会先按脚本执行,再视配置继续探索
官方文档:
11.4 命令行示意(gcloud)
bash
# 跑你自己的仪器化测试 APK(示意)
gcloud firebase test android run \
--type instrumentation \
--app app-debug.apk \
--test app-debug-androidTest.apk \
--device model=Pixel2,version=30
bash
# 跑 Robo,并可带脚本(示意)
gcloud firebase test android run \
--type robo \
--app app-debug.apk \
--device model=Pixel2,version=30 \
--robo-script path/to/script.json
11.5 怎么选这部分?
| 需求 | 选择 |
|---|---|
| 已有 Espresso,想多机型跑 | Test Lab + Instrumentation |
| 发版前随便扫崩溃 | Robo |
| 想引导登录后再探索 | Robo Script |
| 日常开发每个 PR | 不建议只靠云测(慢、有配额) |
12. 方案 I:Gradle Managed Devices(多 Android 版本矩阵)
12.1 它是什么?
在 build.gradle 里声明多台虚拟设备 (不同 API),Gradle 自动:下载系统镜像 → 启动模拟器 → 跑 androidTest → 关掉。
用来回答:「不用堆真机,怎么测多个 Android 版本兼容?」
12.2 如何引入(示意)
groovy
android {
testOptions {
managedDevices {
devices {
// 创建一台名为 phoneApi30 的托管虚拟机
phoneApi30(com.android.build.api.dsl.ManagedVirtualDevice) {
device = "Pixel 2" // 机型配置档
apiLevel = 30 // Android 11
systemImageSource = "aosp-atd" // ATD:更轻量,适合 CI
}
phoneApi34(com.android.build.api.dsl.ManagedVirtualDevice) {
device = "Pixel 6"
apiLevel = 34
systemImageSource = "aosp-atd"
}
}
groups {
// 把多台设备编成一组,一次跑完
apiMatrix {
targetDevices.addAll(devices.phoneApi30, devices.phoneApi34)
}
}
}
}
}
注意:不同 AGP 版本 DSL 写法可能是
localDevices { create("name") { ... } },以你当前 AGP 官方文档为准。上面强调的是含义。
12.3 怎么运行
bash
# 只在某一台上跑
./gradlew phoneApi30DebugAndroidTest
# 跑整个矩阵组(名称随 DSL 生成任务略有不同)
./gradlew apiMatrixGroupDebugAndroidTest
12.4 ATD 是什么?
Automated Test Device :裁剪过的系统镜像,少动画、少杂项,更省 CPU/内存 ,适合 CI。
需要完整 Play 商店能力时再用 google / google-atd 等来源。
12.5 和「真机兼容」的关系
| 能发现 | 不易发现 |
|---|---|
| API 行为差异、权限模型变化 | 厂商 ROM 定制 Bug |
| 布局在不同 API 的表现 | 特殊硬件驱动问题 |
结论:API 兼容用 GMD/模拟器;厂商兼容再用少量真机或云真机抽样。
12.6 要不要自己开不同模拟器?
一般不用。 GMD 由 Gradle 按 build.gradle 声明自动起停虚拟设备;你不必先在 Device Manager 里手动开齐 API 30、API 34 等多台日常 AVD。
| 方式 | 谁管模拟器 |
|---|---|
日常 connectedDebugAndroidTest / 本机脚本 |
你自己先开好一台 AVD(或真机),adb 连上再跑 |
| Gradle Managed Devices | Gradle 管:按矩阵配置起 / 跑 / 停;可并行,吃 CPU 与磁盘 |
注意:
- GMD 测的仍是
androidTest(可与 Espresso 同一套用例),差别在 执行环境怎么来 - 常用 ATD 镜像更轻,适合 CI,不一定是带完整 Play 商店的日常模拟器
- 日常开发仍建议:开一台常用模拟器(或真机)跑 Espresso;GMD 是 多 API 兼容矩阵 的加分项,不是 Maestro / UI Automator 的替代层
- 首次运行会下载系统镜像,可能较久;之后会快很多
12.7 本仓库(zhmy_android_pad)已接入方式
| 项 | 说明 |
|---|---|
| 默认设备 | phoneApi36(Pixel 6 / API 36 / google_apis_playstore,本机已有镜像) |
| 可选设备 | phoneApi30 / phoneApi34(aosp-atd,需先装镜像) |
| 分组 | apiMatrix(默认仅 36);apiMatrixFull(30+34+36,镜像齐后用) |
| 用例 | com.zhmy.pad.gmd.GmdMatrixSmokeTest:登录页离线壳冒烟;清弹窗并重试焦点 |
| 入口 | scripts/test/07_run_gmd_matrix.bat(只跑 package=com.zhmy.pad.gmd) |
| 装镜像 | scripts/test/_install_gmd_images.bat 或 Android Studio → SDK Manager |
| 并发 | maxConcurrentDevices=1;emulator.gpu=swiftshader_indirect |
为何矩阵不跑全量 Espresso?
多设备 × 全量冷启又慢又脆。矩阵只验证「关键壳在多 API 上能起来」;细测仍用 02_run_espresso.bat。
bat
scripts\test\07_run_gmd_matrix.bat
scripts\test\07_run_gmd_matrix.bat -Target api36
scripts\test\07_run_gmd_matrix.bat -Target full
国内网络若拉不下 Google 系统镜像,请用 Studio SDK Manager(可配镜像/代理)安装
aosp_atd后再跑-Target full。
13. 新人推荐学习路线
第 1 周:会写、会跑单元测试
- 学会
src/test+ JUnit - 给一个
Utils写 3 个用例 - 学会看失败堆栈
第 2 周:会写页面点击测试
- 学会 Espresso 三板斧:
onView/perform/check - 写「按钮可见」「空输入提示」两个用例
- 再写一条「登录成功进入下一页」
第 3 周:补系统弹窗与主流程
- 学 UI Automator 点掉权限框
- 可选:用 Maestro 写一条 YAML 冒烟
第 4 周:工程化
- 把测试接到 CI(至少
test任务) - 了解 GMD 或
connectedAndroidTest - 发版前了解 Test Lab / Robo 作为补充
不要一上来就做的事
- 不要先上 Appium(除非跨端硬性要求)
- 不要用 Monkey 当业务验收
- 不要只写 E2E、完全不写单元测试
14. 常见问题 FAQ
Q1:我不想用 AI,能不能自己写?
能。所有方案都是标准工程能力:加依赖 → 建测试类 → 写步骤 → 跑 ▶。
本文示例足够你对照手写。
Q2:模拟器和真机选哪个?
| 场景 | 建议 |
|---|---|
| 日常开发 | 模拟器 |
| 多 API 兼容 | 多模拟器 / GMD |
| 相机、蓝牙、厂商问题 | 真机或云真机 |
Q3:测试账号密码怎么放?
不要把生产密码写进仓库。可用:
- 仅 debug 包可读的
BuildConfig字段 - 本地
local.properties(勿提交) - CI 密钥变量注入
Q4:为什么我的 UI 测试有时过有时不过?
常见原因:动画、网络慢、键盘遮挡、弹窗、异步未等完。
处理:关动画、固定测试数据、显式等待、IdlingResource、避免依赖真实弱网。
Q5:「自动按业务点」到底选哪个?
- 开发自己写、要稳定回归 → Espresso / Compose Test
- 想快速写主路径剧本 → Maestro
- 录一遍就上传云测 → Robo Script
- 随机点找崩溃 → Robo/Monkey(不是业务验收)
- 系统弹窗 / 跨 App → UI Automator(与 Espresso 组合)
三者主责对照见 9.7。
Q6:每个项目都要重新学吗?
不用。套路固定:
- 看 UI 是 View 还是 Compose
- 加对应
androidTestImplementation - 在
androidTest写「启动页 → 操作 → 断言」 - 连上设备跑
换项目只是换包名、Activity、控件 id。
Q7:中文控件用 withText 可以吗?
可以。但文案一改测试就挂;关键控件优先用 R.id 或 Compose testTag。
Q8:文档里说的「冷启」是模拟器重启吗?
不是。 一般指 App / Activity 冷启动 (每条用例重新拉起页面),模拟器可以一直开着。详见 6.11。
Q9:Espresso 为啥不能等网络?非要等可以吗?
默认只等主线程空闲,不等 OkHttp 工作线程。可以靠 IdlingResource 实现等网络;老项目改动可能偏大,也可用「等目标控件出现」折中。详见 6.10。
Q10:每条用例都要重启 Activity 吗?几个 Activity 一起测不行吗?
默认 ActivityScenarioRule 会一测一启,为了隔离。
可以、也推荐:同一次启动内把同页多步合成一条场景 ;不要整包共用一个永不关的 Activity。详见 6.12。
Q11:为啥测试里要写 sleep?
早期多为等异步 UI / 弹窗、换稳定性;代价是全量很慢。应演进为短 settle + 等目标 id 早返回;真等网络用 Idling。详见 6.13、6.14。
Q12:UI Automator 和 Espresso 到底啥区别?
Espresso 测本 App View;UI Automator 管整机(系统弹窗、跨 App)。详见 8.1.1。
Q13:Maestro 又是干啥的?和上面两个怎么分?
YAML 业务剧本,偏端到端主路径;与 Espresso(细测)、UI Automator(系统层)主责不同。详见 9.7。
Q14:Gradle Managed Devices 要自己开很多台模拟器吗?
一般不用 ,Gradle 按配置自动起停;日常手开一台跑 Espresso 即可,GMD 用于多 API 矩阵。详见 12.6、12.7。
本仓库入口:scripts/test/07_run_gmd_matrix.bat(只跑 com.zhmy.pad.gmd 轻量冒烟)。
15. 官方文档入口
| 主题 | 链接 |
|---|---|
| Android 测试总览 | developer.android.com/training/te... |
| Espresso | developer.android.com/training/te... |
| Compose testing | developer.android.com/develop/ui/... |
| UI Automator | developer.android.com/training/te... |
| Robolectric | developer.android.com/training/te... |
| Gradle Managed Devices | developer.android.com/studio/test... |
| Firebase Test Lab | firebase.google.com/docs/test-l... |
| Robo scripts | firebase.google.com/docs/test-l... |
| Maestro | maestro.mobile.dev/ |
| Appium | appium.io/docs/en/lat... |
附录:一张「引入依赖速查」
groovy
dependencies {
// ===== 本地单元测试 src/test =====
testImplementation "junit:junit:4.13.2"
testImplementation "org.robolectric:robolectric:4.13"
// ===== 仪器化测试 src/androidTest =====
androidTestImplementation "androidx.test.ext:junit:1.2.1"
androidTestImplementation "androidx.test:runner:1.6.2"
androidTestImplementation "androidx.test:rules:1.6.1"
androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1"
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.3.0"
// Compose 项目再加 ui-test-junit4(版本与 Compose BOM 对齐)
}
版本号请以 Android Studio 新建工程模板或官方 BOM 为准,定期升级。
附录:最小「业务自动点」对照
同一业务:「打开登录页 → 输入 → 点登录 → 看到首页」
| 方案 | 你要写的东西 |
|---|---|
| Espresso | Java/Kotlin 测试类,onView...perform...check |
| Compose Test | onNodeWithText/Tag...performClick...assertIsDisplayed |
| Maestro | YAML:tapOn / inputText / assertVisible |
| Appium | 独立脚本 + Server + findElement.click |
| Robo Script | Studio 录制 JSON,上传云端 |
任选其一即可实现「像人工、不乱点」。新人优先 Espresso 或 Maestro。