4. Android 测试框架演进:JUnit、AndroidJUnitRunner、Instrumentation 的关系
4.1 为什么需要了解这三者?
做 AOSP 测试,你绕不开三个东西:JUnit、Instrumentation 和 AndroidJUnitRunner。很多人一开始容易搞混,以为它们是一回事。其实不是。
我刚开始接手测试框架改造时,花了好几天才理清它们各自的职责。说实话,当时同事丢给我一个 test runner 配置,我根本不知道 Instrumentation 和 AndroidJUnitRunner 有什么区别。后来踩了几个坑,才慢慢摸出门道。
这一节,我就把它们的演进关系和分工讲清楚。你理解了这个,后面的 Mock 回放设计才能站得住脚。
4.2 JUnit:单元测试的老祖宗
JUnit 是什么?说白了就是 Java 单元测试的标配。它负责写测试用例、跑断言、组织测试套件。在 Android 世界,JUnit 3 和 JUnit 4 都还在用,不过 JUnit 4 已经成了主流。
举个例子,一个简单的 JUnit 4 测试:
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calc = new Calculator();
assertEquals(4, calc.add(2, 2));
}
}
这个测试不依赖任何 Android 框架,纯 Java 就能跑。但在 Android 上测试组件或 Activity 时,你光有 JUnit 不够——**它没法操作 Android 的上下文、生命周期和 UI 线程**。于是 Instrumentation 登场了。
4.3 Instrumentation:Android 测试的“植入器”
Instrumentation 是 Android 系统提供的一个核心类。它允许你像手术刀一样“植入”到应用的进程里,控制 Activity 生命周期、模拟用户操作、获取 UI 控件等。
我习惯把它理解成一个“代理”——它替换了应用的默认 Application,让你能在这个代理进程里执行测试逻辑。早期的 Android 测试框架(比如 android.test 包下的 InstrumentationTestRunner)就是直接基于它。
但 Instrumentation 有个问题:它的 API 比较底层,和 JUnit 集成不够自然。你需要写很多样板代码,比如:
public class MyTest extends InstrumentationTestCase {
public void testSomething() {
getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
}
}
这种写法在 JUnit 4 的注解风格里显得很别扭。于是 Google 做了升级——推出了 AndroidJUnitRunner。
4.4 AndroidJUnitRunner:统一的现代 runner
AndroidJUnitRunner 是 Android Testing Support Library 的核心组件。它把 JUnit 4 的注解和 Instrumentation 的能力结合在了一起。你不用再继承 InstrumentationTestCase,直接写普通的 JUnit 4 测试类,通过注解就能获得 Instrumentation 注入。
我举个例子你就明白了:
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void testButtonClick() {
onView(withText("Click me")).perform(click());
}
}
@RunWith(AndroidJUnit4.class) 这个注解告诉引擎:用它来管理测试生命期。底层它自动帮你启动 Instrumentation,控制 Activity 启动,然后通过 JUnit 的 @Test 执行用例。你不需要手动调用 getInstrumentation(),更不需要写复杂的 setUp/tearDown。
一句话总结:AndroidJUnitRunner 是 Instrumentation 的现代封装,它让测试代码更干净、更容易扩展。**它自己仍然是基于 Instrumentation 的**,不过做了很多自动化的工作。
4.5 三者的关系:一张图讲清楚
为了让你看得更直观,我画了一张关系图。你可以理解成三层结构:
从图中你能看到:JUnit 在最底层提供测试基础能力;AndroidJUnitRunner 在中间层负责编排和执行;Instrumentation 在最上层提供 Android 平台的独有能力。实际运行时,AndroidJUnitRunner 会启动一个 Instrumentation 实例,然后在这个实例内部调用 JUnit 的 runner 来跑测试。
4.6 演进过程的对比表格
我把三个阶段的关键差异总结成一个表格,方便你对比记忆:
| 维度 | JUnit 3 + InstrumentationTestRunner | JUnit 4 + InstrumentationTestRunner | JUnit 4 + AndroidJUnitRunner(当前推荐) |
|---|---|---|---|
| 测试类继承 | 必须继承 InstrumentationTestCase | 可继承,但注解 @Test 可用 | 无需继承,普通 POJO 类即可 |
| Instrumentation 获取 | 手动 getInstrumentation() | 手动获取或通过构造函数 | 自动注入,通过 @Rule 等注解 |
| 生命周期控制 | 手动在 setUp/tearDown 中控制 | 同左 | 通过 ActivityTestRule 自动控制 |
| UI 测试支持 | 需要自己封装 | 需要靠第三方库(如 Robotium) | 内置 Espresso 集成 |
| 测试运行器配置 | AndroidManifest 中指定 test runner | 同左 | 用 @RunWith 注解,更灵活 |
我的个人经验: 在 AOSP 做组件 Mock 回放时,我们强制要求使用 AndroidJUnitRunner。因为它对 JUnit 4 的支持完整,而且能通过自定义 runner 扩展(比如我们内置的 MockPlaybackRunner)。如果还在用旧的 InstrumentationTestRunner,你会发现自己要写大量胶水代码来对接 Mock 数据源,得不偿失。
4.7 避坑指南
- 我曾经遇到过:同事在测试配置里混用了两种 runner——Manifest 里指定的是旧版
android.test.InstrumentationTestRunner,但代码里又写了@RunWith(AndroidJUnit4.class)。结果编译通过,运行时 Instrumentation 始终无法初始化,白白耗了半天。记住:不要在 Manifest 和 @RunWith 中指定不同的 runner。 - 另一坑:如果你自己实现了一个自定义的
AndroidJUnitRunner子类,记得在onCreate()中调用super.onCreate(),否则 Instrumentation 的初始化会中断。我之前在重写startActivity()方法时忘了这步,导致所有 Activity 测试都报空指针。
4.8 总结
这一节,我们把 JUnit、Instrumentation 和 AndroidJUnitRunner 的三层关系拆清楚了。你可以这么记:
- JUnit:写测试的笔和纸。
- Instrumentation:Android 系统的“外科手术工具”。
- AndroidJUnitRunner:帮你把手术工具和笔纸无缝对接的手术师。
在后面的章节,我们会基于 AndroidJUnitRunner 来做自定义 runner,这里面会涉及到 Mock 回放的注入点选择。搞明白这三者的关系,对我们后续设计 Mock 回放架构至关重要。