Android 自动化测试完全指南(新人版)

Android 自动化测试完全指南(新人版)

写给谁看 :刚入门的 Android 开发,即使从未写过测试,也能按本文一步步在任意项目里手写自动化测试。

本文目标 :讲清「每种方案是什么、怎么引入、怎么手写、怎么跑、怎么选」。

示例约定 :文中代码全部使用虚构的演示包名 com.example.demoapp不依赖任何真实业务工程,可直接照着改到你自己的项目。


目录

  1. 先建立正确认知
  2. 一张表:怎么选方案
  3. [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")
  4. [方案 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")
  5. [方案 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")
  6. [方案 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")
  7. [方案 D:Compose UI Test](#方案 D:Compose UI Test "#7-%E6%96%B9%E6%A1%88-dcompose-ui-test")
  8. [方案 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")
  9. [方案 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")
  10. [方案 G:Appium(跨端黑盒)](#方案 G:Appium(跨端黑盒) "#10-%E6%96%B9%E6%A1%88-gappium%E8%B7%A8%E7%AB%AF%E9%BB%91%E7%9B%92")
  11. [方案 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")
  12. [方案 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")
  13. 新人推荐学习路线
  14. [常见问题 FAQ](#常见问题 FAQ "#14-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-faq")
  15. 官方文档入口

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 ScriptF Maestro C
同一套 UI 测试跑多个 Android 版本(可不靠真机) I GMD + C/D H 云测

2.1 一句话决策

  1. 只测逻辑 → JUnit
  2. 测自己 App 里的页面(View) → Espresso
  3. 测 Compose 页面 → Compose Test
  4. 测主流程、想写得简单 → Maestro
  5. 碰到系统弹窗 / 多 App → UI Automator
  6. 要 iOS 也一起 → Appium
  7. 发版前多机型扫一遍 → Firebase Test Lab
  8. 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):

  1. 打开测试类,点类名或方法左边的绿色三角形 ▶
  2. 或顶部菜单:Run → 选择对应测试
  3. 或 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 用,不会打进正式 APK
  • junit:提供 @TestAssert.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 模式(强烈建议新人一直用):

  1. Arrange:准备数据
  2. Act:调用方法
  3. 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 怎么运行

  1. 先开一个模拟器,或插上真机并打开 USB 调试
  2. 运行:./gradlew connectedDebugAndroidTest
  3. 或只跑某一个类(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 冷启动 :每条 @TestActivityScenarioRule(或 launch)重新拉起页面
  • 不是每次把 AVD / 模拟器进程关掉再开

模拟器可以一直开着、已解锁;耗时大头往往是「每条用例重新创建 Activity」。

6.12 为啥每条用例都重启 Activity?几个页面能不能一起测?

ActivityScenarioRule 的默认行为是:每个 @Test 开始启动、结束关闭

目的是换 隔离:上一条点进改密、开了弹窗、退出登录,下一条不该带着脏状态;失败也好定位。

做法 说明
同一次启动里多步合成一条场景(推荐) 例如一次进设置,扫完多个子菜单再结束。少冷启,风险可控
一条用例里先后 launch 多个 Activity 可以,仍是按需启停,只是写在同一个 @Test
整个测试类共用一个永不关的 Activity 最快,但串状态、失败难查、退出登录后全挂;一般不建议当默认

破坏性步骤(如 signOut、会 finish 的 pressBack)应 单独成条、放最后,不要和前面的菜单冒烟硬串。

6.13 当初为啥要 sleep?现在怎么等更合适?

早期加固定 sleep,通常是为了 先把冒烟跑稳,而不是故意拖时间:

  1. 等异步 UI:Fragment 切换、列表刷新、弹窗,Espresso 默认同步不到你们业务异步
  2. 挡系统/业务弹窗 :权限、「知道了」等会抢焦点;旧写法对每个文案 wait(超时),没弹窗也空等满超时,全量会被拖很慢
  3. 模拟器更抖:按慢设备把等待拉长,减少偶发失败
  4. 没有 IdlingResource 时的权宜之计:老工程改造成本高时,sleep / 轮询最省事

更合适的演进:

  • 弹窗:瞬时 hasObject,有再点
  • 启动后:轻量 settleUi(扫弹窗 + 很短一帧)
  • 切页后:waitForRes / 等目标 id 出现即返回,少整秒级固定 sleep
  • 真要稳等网络:集中挂 IdlingResource

6.14 写 Espresso 时:风险小、耗时可控的建议

  1. 少冷启:同页多断言合并成场景测;日常只跑相关类,发版再全量
  2. 早返回等待:轮询目标控件,避免「没东西也空等」
  3. 选择器对真实 UI :重复 id、空文案宽 0、EmptyRecyclerView 空态 GONE 等,按真机表现写
  4. 破坏性操作隔离:退出登录等单独用例
  5. 环境 :模拟器已开已解锁;animationsDisabled true;离线冒烟尽量 seed 本地会话,少绑死服务器
  6. 不要为了省启动让所有用例共享一份可变的全局界面状态

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 怎么配合?

常见模式:

  1. Espresso 点到会触发权限的按钮
  2. UI Automator 点系统「允许」
  3. 再回到 Espresso 继续业务断言

8.5 何时不要用它硬测业务页?

纯 App 内页面优先 Espresso/Compose:更快、同步更好、更少 flaky。


9. 方案 F:Maestro(YAML 业务剧本)

9.1 它是什么?

YAML 文件 描述「用户操作步骤」,像写产品验收清单。

非常适合:有目的的主流程冒烟(登录、下单、关键 Tab)。

不写 Java 也能做出「像人点」的效果。

9.2 适合新人吗?

适合。尤其当你想快速固化「主路径」,又不想先学一整套 Espresso API。

9.3 如何安装(本机)

  1. 安装 Maestro CLI(以官方安装方式为准,常见为):
bash 复制代码
curl -Ls "https://get.maestro.mobile.dev" | bash

Windows 请查看 Maestro 官方文档 的安装说明(可用包管理器或发布包)。

  1. 确保已连接模拟器或真机:adb devices 能看到设备。
  2. 安装并启动你的 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

新人需要准备:

  1. Node.js(常用于安装 Appium)
  2. Appium Server
  3. Android SDK / 模拟器或真机
  4. 一种语言的客户端库(如 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 为准,菜单名随版本可能微调):

  1. 安装 Firebase / Test Lab 相关插件(若需要)
  2. Record Robo Script 功能,在模拟器上亲手走一遍业务
  3. 得到 JSON 脚本文件
  4. 上传 APK + 脚本到 Firebase Test Lab
  5. 云端会先按脚本执行,再视配置继续探索

官方文档:

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 版本兼容?」

官方文档:Gradle managed devices

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 / phoneApi34aosp-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=1emulator.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 周:会写、会跑单元测试

  1. 学会 src/test + JUnit
  2. 给一个 Utils 写 3 个用例
  3. 学会看失败堆栈

第 2 周:会写页面点击测试

  1. 学会 Espresso 三板斧:onView / perform / check
  2. 写「按钮可见」「空输入提示」两个用例
  3. 再写一条「登录成功进入下一页」

第 3 周:补系统弹窗与主流程

  1. 学 UI Automator 点掉权限框
  2. 可选:用 Maestro 写一条 YAML 冒烟

第 4 周:工程化

  1. 把测试接到 CI(至少 test 任务)
  2. 了解 GMD 或 connectedAndroidTest
  3. 发版前了解 Test Lab / Robo 作为补充

不要一上来就做的事

  • 不要先上 Appium(除非跨端硬性要求)
  • 不要用 Monkey 当业务验收
  • 不要只写 E2E、完全不写单元测试

14. 常见问题 FAQ

Q1:我不想用 AI,能不能自己写?

能。所有方案都是标准工程能力:加依赖 → 建测试类 → 写步骤 → 跑 ▶。

本文示例足够你对照手写。

Q2:模拟器和真机选哪个?

场景 建议
日常开发 模拟器
多 API 兼容 多模拟器 / GMD
相机、蓝牙、厂商问题 真机或云真机

Q3:测试账号密码怎么放?

不要把生产密码写进仓库。可用:

  • 仅 debug 包可读的 BuildConfig 字段
  • 本地 local.properties(勿提交)
  • CI 密钥变量注入

Q4:为什么我的 UI 测试有时过有时不过?

常见原因:动画、网络慢、键盘遮挡、弹窗、异步未等完。

处理:关动画、固定测试数据、显式等待、IdlingResource、避免依赖真实弱网。

另见 6.106.13

Q5:「自动按业务点」到底选哪个?

  • 开发自己写、要稳定回归 → Espresso / Compose Test
  • 想快速写主路径剧本 → Maestro
  • 录一遍就上传云测 → Robo Script
  • 随机点找崩溃 → Robo/Monkey(不是业务验收)
  • 系统弹窗 / 跨 App → UI Automator(与 Espresso 组合)

三者主责对照见 9.7

Q6:每个项目都要重新学吗?

不用。套路固定:

  1. 看 UI 是 View 还是 Compose
  2. 加对应 androidTestImplementation
  3. androidTest 写「启动页 → 操作 → 断言」
  4. 连上设备跑

换项目只是换包名、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.136.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.612.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


相关推荐
码云骑士1 小时前
76-全量微调vs-LoRA-vs-QLoRA-三种微调方式对比与选型
android
光头闪亮亮2 小时前
Fyne ( go跨平台GUI )项目实战-WebView 组件开发技术详解
android·go
嵌入式小周2 小时前
Genymotion 安卓模拟器在 Intel 芯片 Mac 上的运行(附带下载方式)
android·macos
zzq77974 小时前
Android 16 API 36 升级后 APP 加固兼容性问题解析
android·开发语言·安全·kotlin·安卓·安全架构
2601_961391465 小时前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·人工智能
2501_9159184114 小时前
深入对比iOS开发中常用性能监控工具的底层原理与优缺点分析
android·ios·小程序·https·uni-app·iphone·webview
my_power52015 小时前
android中Activity生命周期函数的职责
android
Sirens.15 小时前
从参考 iCost 到做自己的 OneLedger:一个 Android 本地记账 App 的开发记录
android·kotlin·room·jetpack compose·记账 app
qq_4480111615 小时前
C语言中的变量和函数的定义与声明
android·c语言·开发语言