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 关系图 JUnit 4/5 编写测试用例、断言、测试套件 AndroidJUnitRunner 管理测试生命期,解析 @RunWith、@Rule 等 Instrumentation 控制 Activity 生命周期、注入 Context、模拟输入 传统方式(已废弃) 继承 InstrumentationTestCase → 直接操作 Instrumentation → 手动管理生命周期 现代方式(推荐) 用 @RunWith(AndroidJUnit4.class) → AndroidJUnitRunner 自动调度 Instrumentation 测试报告 / 结果 依赖关系

从图中你能看到: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 回放架构至关重要。