12、Activity与Fragment测试:ActivityScenario与FragmentScenario、测试Activity生命周期、测试Fragment导航、测试屏幕旋转、测试SavedInstanceState
说实话,Activity和Fragment的测试,是Android单元测试里最绕不开的一环。你想想看,用户看到的每个界面,背后都是它们在撑着。如果这些组件没测好,那崩溃、状态丢失、导航错乱……这些坑你迟早会踩一遍。
我个人习惯把这类测试叫做「组件级集成测试」。它不像纯单元测试那样只测一个类,也不像UI自动化那样跑全流程。它聚焦在Activity或Fragment本身的行为上——生命周期怎么走、状态怎么存、旋转后怎么恢复。说白了,就是保证你的页面组件在各种系统事件下都能稳住。
ActivityScenario:启动与生命周期控制
以前我们用ActivityController,后来官方推荐了ActivityScenario。这东西用起来很顺手,它能帮你精确控制Activity的生命周期状态。
@Test
fun testActivityLifecycle() {
val scenario = ActivityScenario.launch(MainActivity::class.java)
// 当前状态:RESUMED
scenario.moveToState(Lifecycle.State.CREATED)
// 现在Activity处于onCreate之后、onStart之前
scenario.moveToState(Lifecycle.State.STARTED)
// onStart已调用
scenario.moveToState(Lifecycle.State.RESUMED)
// 回到前台可见状态
scenario.close()
}
嗯,这里要注意:moveToState是同步的,调用后状态立即切换。我在项目中遇到过一个问题——测试里调了moveToState(DESTROYED)后,有些异步任务还没取消,导致内存泄漏。后来我养成了一个习惯:在@After里统一调用scenario.close(),确保资源释放。
核心要点:ActivityScenario 会自动帮你处理主线程调度,你不需要手动调用runOnUiThread。但如果你要操作UI控件,还是得用onActivity { }回调。
FragmentScenario:轻量级Fragment测试
Fragment测试比Activity更灵活。你可以单独启动一个Fragment,不依赖它的宿主Activity。这就是FragmentScenario的价值所在。
@Test
fun testFragmentNavigation() {
val scenario = FragmentScenario.launchInContainer(DetailFragment::class.java)
scenario.onFragment { fragment ->
// 在这里可以直接访问fragment的成员变量
val button = fragment.view?.findViewById<Button>(R.id.btn_next)
assertNotNull(button)
}
}
你可能会问:那Fragment的导航怎么测?我一般用NavController来做。把NavController注入到Fragment里,然后验证导航动作。
@Test
fun testNavigationToDetail() {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
val scenario = FragmentScenario.launchInContainer(ListFragment::class.java)
scenario.onFragment { fragment ->
navController.setGraph(R.navigation.nav_graph)
fragment.viewLifecycleOwnerLiveData.observeForever { lifecycleOwner ->
Navigation.setViewNavController(fragment.requireView(), navController)
}
// 模拟点击跳转
fragment.view?.findViewById<Button>(R.id.btn_detail)?.performClick()
assertEquals(R.id.detailFragment, navController.currentDestination?.id)
}
}
我曾经踩过一个坑:Fragment的viewLifecycleOwner在onCreateView之后才可用。如果你在onFragment回调里直接拿findNavController(),可能会拿到null。我的建议是:等viewLifecycleOwner.lifecycle.currentState至少是STARTED再操作。
测试屏幕旋转:状态保持的试金石
屏幕旋转是Android开发里最经典的「坑」。用户转一下手机,Activity重建了,数据丢了,界面乱了。测试这个场景,ActivityScenario提供了recreate()方法。
@Test
fun testRotationPreservesData() {
val scenario = ActivityScenario.launch(EditActivity::class.java)
scenario.onActivity { activity ->
val editText = activity.findViewById<EditText>(R.id.et_content)
editText.setText("旋转后还在吗?")
}
// 模拟屏幕旋转
scenario.recreate()
scenario.onActivity { activity ->
val editText = activity.findViewById<EditText>(R.id.et_content)
assertEquals("旋转后还在吗?", editText.text.toString())
}
}
这里有个细节:recreate()会先销毁Activity,再重新创建。如果你的数据没有通过onSaveInstanceState保存,那旋转后肯定丢了。我见过不少项目只在onCreate里恢复数据,但忘了在onSaveInstanceState里保存——结果就是旋转后一片空白。
注意:如果Activity里用了ViewModel,旋转后ViewModel实例不会销毁,数据自然保留。但EditText这种View自带的状态(比如输入的文字),系统默认会帮你保存和恢复。前提是你没有在布局里设置freezeText="false"。
测试SavedInstanceState:手动模拟状态保存与恢复
有时候你想更精细地控制——比如模拟系统在低内存下杀死进程,然后恢复。这时候可以手动构造一个Bundle,模拟onSaveInstanceState的流程。
@Test
fun testSavedInstanceState() {
val scenario = ActivityScenario.launch(DetailActivity::class.java)
// 保存状态
var savedState: Bundle? = null
scenario.onActivity { activity ->
savedState = Bundle()
activity.onSaveInstanceState(savedState)
}
// 销毁Activity
scenario.moveToState(Lifecycle.State.DESTROYED)
// 用保存的状态重新启动
val restoredScenario = ActivityScenario.launch(DetailActivity::class.java, savedState)
restoredScenario.onActivity { activity ->
// 验证恢复后的数据
val title = activity.findViewById<TextView>(R.id.tv_title)
assertNotNull(title)
}
restoredScenario.close()
}
我个人觉得,手动测试SavedInstanceState比单纯依赖recreate()更彻底。因为recreate()走的是系统完整的重建流程,而手动构造Bundle可以让你精确控制「哪些数据被保存了,哪些没有」。我曾经用这个方法发现过一个bug:某个Fragment的onSaveInstanceState里保存了数据,但onCreate里恢复时用了错误的key——这种问题靠recreate()很难复现。
知识体系总览
下面这张图梳理了本章的核心内容,方便你对照复习:
避坑指南与最佳实践
最后,分享几个我这些年积累下来的经验:
- 测试不要依赖真实的Handler延迟。 用
ActivityScenario时,所有生命周期切换都是同步的。如果你在代码里用了Handler.postDelayed,测试里等不到那个延迟——建议用CountDownLatch或IdlingResource来同步。 - Fragment测试里慎用
requireActivity()。 用FragmentScenario.launchInContainer启动的Fragment,它的宿主Activity是一个空壳。如果你在Fragment里调了requireActivity().intent,会拿到null。我建议把这种依赖通过参数或接口注入进来。 - 屏幕旋转测试要覆盖「快速连续旋转」。 我曾经遇到一个bug:快速旋转两次,Activity的
onDestroy和onCreate交错执行,导致ViewModel的onCleared被调用了两次。测试里连续调两次recreate()就能复现。 - SavedInstanceState的Bundle不要放太大对象。 系统对Bundle有大小限制(大约1MB)。如果你存了一个Bitmap进去,旋转时直接崩溃。测试里可以构造一个超大的Bundle,验证你的代码会不会崩溃。
小技巧:如果你用Robolectric跑这些测试,速度会比真机快很多。但要注意,Robolectric对SavedStateHandle的支持不太完善,建议在真机或模拟器上再跑一遍。
好了,这一章的内容就到这里。Activity和Fragment的测试,说白了就是模拟系统对组件的各种「折腾」——启动、停止、旋转、销毁、恢复。你把这些场景都覆盖到了,你的页面组件就稳了。