第四讲:androidx.test.core 核心库的测试规则与工具
测试框架用多了你会发现,测试规则(Rule)才是真正提升效率的钥匙。androidx.test.core 这个库,说白了就是 Google 为我们准备的一套“预制件”——不需要重复造轮子,开箱即用。我个人习惯把这块内容分成两类:生命周期控制型和上下文模拟型。今天我们就逐个拆解。
1. ActivityScenarioRule —— 活动生命周期操控器
之前写测试,要启动一个 Activity 得自己写 InstrumentationRegistry 和 Intent 配合,代码又臭又长。ActivityScenarioRule 直接帮你干了这件事。用法很简单:
@get:Rule
var activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testLifecycle() {
activityRule.scenario.onActivity { activity ->
assertNotNull(activity.findViewById(R.id.button))
}
}
我遇到过一个问题:测试写完后发现每次启动 Activity 都会重建,数据库也跟着重置。后来才发现是没有控制测试隔离。注意,这条规则默认会 终止上一个场景再启动新场景,如果你希望复用 Activity 对象,可以自己管理 scenario。
核心要点:ActivityScenarioRule 会自动调用 ActivityScenario.launch(),并在 @After 时调用 close()。你可以在 test 方法中通过 scenario.moveToState() 手动切换状态(CREATED / STARTED / RESUMED / DESTROYED)。
2. ServiceTestRule —— 服务绑定与启动规则
Service 测试一直是头疼的事情。ServiceTestRule 提供了两个主要方法:bindService() 和 startService()。我建议你用 bindService() 来测试接口回调,用 startService() 来测试后台逻辑。
@get:Rule
var serviceRule = ServiceTestRule()
@Test
fun testBindService() {
val intent = Intent(ApplicationProvider.getApplicationContext(),
MyService::class.java)
val binder = serviceRule.bindService(intent)
val myService = (binder as MyService.LocalBinder).getService()
assertEquals("expected", myService.getData())
}
经验之谈:我踩过最大的坑是在 bindService() 里忘记设置 Context.BIND_AUTO_CREATE 标志,结果 Service 根本没创建。ServiceTestRule 内部使用 Context.bindService() 时默认带了这个标志,但如果你手动传 Intent 时加了其他 flag,可能会覆盖掉。
ServiceTestRule 和 ActivityScenarioRule 都是 ExternalResource 的扩展,所以你可以把它们和其他自定义 Rule 组合使用——比如先启动 Service,再启动 Activity。
3. ActivityTestRule(已弃用)与迁移建议
老项目里可能还见到 ActivityTestRule<T>,这个类在 android.test 包中,androidx.test.core 1.4.0 之后标记为弃用。为什么要弃用?因为它默认在 @Before 之前创建 Activity,导致无法控制生命周期。而 ActivityScenarioRule 把控制权交给了你。
如果你还在维护遗留代码,建议尽快迁移。迁移步骤很简单:
- 将
@get:Rule行中的ActivityTestRule替换为ActivityScenarioRule - 把
getActivity()调用改成activityRule.scenario.onActivity { } - 如果之前用了
getInstrumentation()等,改用ApplicationProvider
4. ApplicationProvider —— 上下文工具
测试中最烦的就是获取 Application 或 Context。ApplicationProvider 是 Google 提供的静态工具类,它底层依赖 InstrumentationRegistry.getTargetContext(),但写法更优雅:
val context = ApplicationProvider.getApplicationContext<Context>()
val app = ApplicationProvider.getApplicationContext<MyApplication>()
记得在测试启动前,ApplicationProvider 必须被一个 AndroidJUnitRunner 初始化。如果单独跑 JUnit 单元测试,这个调用会抛异常。
警告:不要在 @BeforeClass 静态方法里调用 ApplicationProvider,因为它依赖 Instrumentation,此时可能还没初始化。建议放在 @Before 方法中。
5. 综合对比表
| 工具/规则 | 用途 | 生命周期控制度 | 常用场景 |
|---|---|---|---|
| ActivityScenarioRule | 启动并操控 Activity | 强(可手动调状态) | UI 测试、碎片交互 |
| ServiceTestRule | 启动或绑定 Service | 中(只提供启动/关闭) | 后台逻辑、接口回调 |
| ApplicationProvider | 获取 Application/Context | 无(仅工具类) | 资源读取、DI 容器构建 |
| ActivityTestRule(弃用) | 旧版 Activity 启动 | 弱(自动创建) | 遗留项目迁移 |
6. 知识结构图
下面这张图帮你理清这几个核心组件的关系,以及你写测试时该怎么选:
7. 实战避坑指南
- 我曾经在 CI 上跑测试一直报 NoClassDefFoundError——后面发现是 Gradle 依赖忘了加
androidx.test:rules。注意:ActivityScenarioRule 在core库,但 ServiceTestRule 在rules库。 - 使用 ServiceTestRule 时,如果 Service 里用到了 HandlerThread,你需要确保测试等待足够时间。我一般配合
IdlingRegistry或CountDownLatch使用。 - ApplicationProvider 在 Robolectric 下也能工作,但它返回的是
RuntimeEnvironment.application,不是真正的 Android 应用。如果你做纯单元测试,建议用 Mockito 模拟 Context。
一句话总结:androidx.test.core 提供的三个核心武器——ActivityScenarioRule (生命周期全操控)、ServiceTestRule (服务绑定/启动)、ApplicationProvider (上下文获取)——基本覆盖了 90% 的 instrumentation 测试场景。剩下的 10% 靠你组合自定义 Rule 来解决。