18、导航测试:测试导航动作、测试Safe Args生成的Direction、使用Navigation测试库、编写UI测试
导航组件用起来确实爽,但有个问题——你怎么保证它真的按你设计的路线走?
我见过太多项目,导航图画得漂漂亮亮,一跑起来就跳错页面。说白了,导航也需要测试,而且得系统性地测。
这一章,我们就聊聊怎么给导航写测试。包括测导航动作、测Safe Args生成的Direction、用Navigation测试库,以及写完整的UI测试。
为什么导航需要单独测试?
你可能会想:「我点个按钮跳转页面,手动测一下不就行了?」
嗯,小项目确实可以。但项目一大,页面跳转链路复杂起来,手动测试就变成噩梦了。我有个项目,导航图里嵌套了3层graph,光手动验证一遍就要20分钟。
更关键的是,导航测试能帮你捕获两类问题:
- 路由错误:目标页面ID写错了,或者参数类型不匹配
- 状态丢失:跳转过程中ViewModel的状态没正确传递
这两类问题,靠肉眼很难发现。但写个测试用例,几秒钟就能跑出来。
测试导航动作
导航动作,说白了就是你在NavGraph里定义的<action>标签。比如从A页面跳到B页面。
测试导航动作,核心思路是:模拟一个NavController,然后触发导航,验证是否跳到了正确的目的地。
核心思路:使用TestNavHostController替代真实的NavController,然后验证currentDestination或backStack。
来看一个例子。假设我们有一个HomeFragment,点击按钮后导航到DetailFragment:
// 在测试中
@RunWith(AndroidJUnit4::class)
class NavigationTest {
@get:Rule
val navComposeTestRule = createComposeRule()
private lateinit var navController: TestNavHostController
@Before
fun setup() {
navController = TestNavHostController(
ApplicationProvider.getApplicationContext()
)
navController.setGraph(R.navigation.nav_graph)
}
@Test
fun testNavigateToDetail() {
// 模拟点击导航按钮
composeTestRule.onNodeWithText("查看详情").performClick()
// 验证是否跳到了DetailFragment
assertEquals(
R.id.detailFragment,
navController.currentDestination?.id
)
}
}
这里有个坑——TestNavHostController需要手动设置graph,否则它不知道你的导航图长什么样。我刚开始用的时候忘了这步,结果测试一直报空指针,排查了半天。
测试Safe Args生成的Direction
Safe Args是导航组件的精华。它帮你生成类型安全的Direction类,避免传参时拼错key或者类型不匹配。
但问题来了——你怎么知道生成的Direction是对的?
嗯,其实Safe Args生成的代码本身是可靠的,但你的使用方式可能有问题。比如:
- 传了错误的参数值
- 参数类型不匹配(比如传了String但期望Int)
- 忘记传必填参数
测试Direction,我一般这样做:
@Test
fun testSafeArgsDirection() {
// 构造一个Direction
val direction = HomeFragmentDirections.actionHomeToDetail(
itemId = 42,
itemName = "测试商品"
)
// 验证参数是否正确
assertEquals(42, direction.arguments?.getInt("itemId"))
assertEquals("测试商品", direction.arguments?.getString("itemName"))
}
你可能会问:「这测了有啥用?不就是验证getter吗?」
有用。我遇到过一种情况:重构时改了参数名,但忘了更新调用方。Safe Args编译期会报错,但如果你用了反射或者动态传参,编译期就抓不到了。这时候单元测试能帮你兜底。
小技巧:如果你用navArgs()在Fragment里取参数,可以写一个测试验证参数解析是否正确。我曾经因为参数默认值没设置对,导致线上页面白屏,后来加了这层测试就再也没出过问题。
使用Navigation测试库
Google官方提供了一个navigation-testing库,里面有几个好东西:
TestNavHostController:模拟NavController,支持设置graph和验证导航NavControllerSpy:可以监听导航事件,验证是否调用了navigate()NavDeepLinkBuilder:用于测试Deep Link导航
引入依赖:
// build.gradle (app)
androidTestImplementation "androidx.navigation:navigation-testing:2.7.7"
使用NavControllerSpy的例子:
@Test
fun testNavigationWithSpy() {
val spy = NavControllerSpy(
ApplicationProvider.getApplicationContext()
)
spy.setGraph(R.navigation.nav_graph)
// 执行导航
spy.navigate(R.id.detailFragment)
// 验证是否调用了navigate
assertTrue(spy.navigateCallCount > 0)
assertEquals(R.id.detailFragment, spy.lastNavigatedTo)
}
这个NavControllerSpy特别适合验证「导航是否被触发」,而不关心具体跳到了哪里。比如你想测试某个按钮是否绑定了正确的点击事件。
编写UI测试
UI测试是最后一道防线。它模拟用户操作,验证整个导航流程是否正常。
我一般用FragmentScenario或者ActivityScenario来启动Fragment,然后模拟点击、滑动等操作。
来看一个完整的UI测试例子:
@RunWith(AndroidJUnit4::class)
class NavigationUiTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testFullNavigationFlow() {
// 启动Activity
val scenario = ActivityScenario.launch(MainActivity::class.java)
scenario.onActivity { activity ->
// 获取NavController
val navController = activity.findNavController(R.id.nav_host_fragment)
// 验证初始页面
assertEquals(R.id.homeFragment, navController.currentDestination?.id)
// 模拟点击跳转
onView(withId(R.id.btn_to_detail)).perform(click())
// 验证跳转后的页面
assertEquals(R.id.detailFragment, navController.currentDestination?.id)
// 验证页面上的UI元素
onView(withId(R.id.tv_title)).check(matches(withText("详情页面")))
}
}
}
这里有个关键点——不要直接依赖Fragment的类名。因为Fragment可能被替换或者重构,你应该依赖R.id来验证目的地。
注意:UI测试跑起来比较慢,建议只覆盖核心导航路径。我一般只测3-5条关键路径,比如「首页→详情→支付→成功」这种。细枝末节的跳转交给单元测试就好。
知识体系总览
下面这张图总结了导航测试的核心内容:
避坑指南
最后分享几个我踩过的坑:
- 不要用真实NavController测试:真实NavController会尝试加载Fragment,导致测试变慢且容易出错。用
TestNavHostController就好。 - 记得设置graph:
TestNavHostController不会自动加载你的导航图,必须手动调用setGraph()。 - 参数验证要全面:Safe Args生成的Direction,不仅要验证参数值,还要验证参数类型。我遇到过String传成了Int,编译期没报错,运行时直接崩溃。
- UI测试不要贪多:UI测试跑一次可能要几十秒,全量跑一遍能花半小时。挑核心路径测,其他交给单元测试。
导航测试这件事,说白了就是给你的导航图加个「安全带」。刚开始写可能觉得麻烦,但一旦出过线上事故,你就会发现——这时间花得值。
我的习惯:每次新增一个导航动作,顺手写一个测试。这样积少成多,测试覆盖率自然就上去了。别等到项目后期再补,那时候补测试比写新代码还痛苦。
公众号:蓝海资料掘金营,微信deep3321