14、Navigation组件测试:测试导航图、测试NavHost、测试DeepLink、测试返回栈、测试导航参数传递
Navigation 组件,说白了就是帮我们管理 Fragment 跳转的。以前我们写 Intent、写 FragmentTransaction,代码又臭又长,还容易出 bug。Navigation 把这些都封装好了,但问题来了——你怎么保证你画的导航图是对的?DeepLink 能不能正常跳?返回栈会不会乱?
嗯,这就是今天我们要聊的。我会从最基础的导航图测试开始,一步步带你搞定 Navigation 组件的全套测试方案。
- 测试导航图的结构和合法性
- 测试 NavHost 的 Fragment 加载
- 测试 DeepLink 跳转
- 测试返回栈行为
- 测试导航参数传递
14.1 测试导航图:别让 XML 坑了你
导航图就是个 XML 文件,里面定义了 Fragment 之间的跳转关系。你想想看,如果 XML 写错了——比如目标 Fragment 不存在、action ID 重复了——那运行时直接崩。所以第一步,我们要验证导航图本身是合法的。
我个人习惯用 NavGraphTestRule 来测。这个规则会加载导航图,然后检查它的结构。
@RunWith(AndroidJUnit4::class)
class NavGraphTest {
@get:Rule
val navGraphRule = NavGraphTestRule(
R.navigation.nav_graph_main
)
@Test
fun testNavGraphHasCorrectStartDestination() {
// 验证起始目的地
val startDest = navGraphRule.navGraph.startDestinationId
assertThat(startDest, `is`(R.id.homeFragment))
}
@Test
fun testNavGraphContainsAllDestinations() {
// 检查所有 Fragment 是否都在导航图中
val destinations = navGraphRule.navGraph.nodes.keys
assertThat(destinations, hasItem(R.id.homeFragment))
assertThat(destinations, hasItem(R.id.detailFragment))
assertThat(destinations, hasItem(R.id.settingsFragment))
}
@Test
fun testActionIdsAreUnique() {
// 检查 action ID 是否重复
val actions = navGraphRule.navGraph.actions.keys
val uniqueActions = actions.toSet()
assertThat(actions.size, `is`(uniqueActions.size))
}
}
14.2 测试 NavHost:Fragment 真的加载了吗?
NavHost 是 Navigation 的容器,它负责把 Fragment 塞进去。测试 NavHost 说白了就是验证:当导航发生时,正确的 Fragment 被加载到了正确的位置。
这里我推荐用 NavHostTestRule 或者 FragmentScenario。我个人更倾向后者,因为它更灵活。
@RunWith(AndroidJUnit4::class)
class NavHostTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testNavHostLoadsHomeFragment() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navHostFragment = activity.supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
// 验证当前 Fragment 是 HomeFragment
val currentFragment = navHostFragment.childFragmentManager
.fragments.firstOrNull()
assertThat(currentFragment, instanceOf(HomeFragment::class.java))
}
}
@Test
fun testNavigateToDetailFragment() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// 执行导航
navController.navigate(R.id.action_home_to_detail)
// 验证当前目的地
assertThat(
navController.currentDestination?.id,
`is`(R.id.detailFragment)
)
}
}
}
onCreate 还没执行完就去拿 NavController,结果拿了个 null,排查了半天。用 onActivity 回调可以避免这个问题。
14.3 测试 DeepLink:外部跳转稳不稳?
DeepLink 是 Navigation 的一大亮点——你可以通过 URL 或者 Intent 直接跳转到某个 Fragment。但问题来了:DeepLink 的 URI 写对了没?参数传对了没?
测试 DeepLink 的核心思路是:构造一个 Intent,模拟外部应用发起的跳转,然后验证是否到达了正确的目的地。
@RunWith(AndroidJUnit4::class)
class DeepLinkTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testDeepLinkOpensDetailFragment() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// 构造 DeepLink Intent
val deepLinkIntent = Intent(Intent.ACTION_VIEW)
deepLinkIntent.data = Uri.parse("myapp://detail/123")
// 处理 DeepLink
val handled = navController.handleDeepLink(deepLinkIntent)
// 验证 DeepLink 被处理
assertThat(handled, `is`(true))
// 验证跳转到了 DetailFragment
assertThat(
navController.currentDestination?.id,
`is`(R.id.detailFragment)
)
}
}
@Test
fun testDeepLinkWithInvalidUri() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// 构造一个不存在的 DeepLink
val invalidIntent = Intent(Intent.ACTION_VIEW)
invalidIntent.data = Uri.parse("myapp://nonexistent")
val handled = navController.handleDeepLink(invalidIntent)
// 验证 DeepLink 没有被处理
assertThat(handled, `is`(false))
}
}
}
14.4 测试返回栈:别让用户回不去
返回栈是 Navigation 最容易被忽视的部分。你想想看,用户从 A 跳到 B,再跳到 C,按返回键应该回到 B,而不是直接退出 App。如果返回栈乱了,用户体验直接崩。
测试返回栈,说白了就是验证 popBackStack() 的行为是否符合预期。
@RunWith(AndroidJUnit4::class)
class BackStackTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testBackStackReturnsToPreviousFragment() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// 从 Home 跳到 Detail
navController.navigate(R.id.action_home_to_detail)
assertThat(
navController.currentDestination?.id,
`is`(R.id.detailFragment)
)
// 按返回键
navController.popBackStack()
// 验证回到了 Home
assertThat(
navController.currentDestination?.id,
`is`(R.id.homeFragment)
)
}
}
@Test
fun testBackStackWithMultipleNavigations() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// A -> B -> C
navController.navigate(R.id.action_home_to_detail)
navController.navigate(R.id.action_detail_to_settings)
// 验证当前在 C
assertThat(
navController.currentDestination?.id,
`is`(R.id.settingsFragment)
)
// 返回两次,应该回到 A
navController.popBackStack()
navController.popBackStack()
assertThat(
navController.currentDestination?.id,
`is`(R.id.homeFragment)
)
}
}
@Test
fun testPopUpToClearsBackStack() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// A -> B -> C
navController.navigate(R.id.action_home_to_detail)
navController.navigate(R.id.action_detail_to_settings)
// 使用 popUpTo 回到 A,并清除中间页面
val navOptions = NavOptions.Builder()
.setPopUpTo(R.id.homeFragment, inclusive = false)
.build()
navController.navigate(R.id.action_settings_to_home, null, navOptions)
// 验证返回栈中只有 A
val backStack = navController.backStack
assertThat(backStack.size, `is`(1))
assertThat(
backStack.first().destination.id,
`is`(R.id.homeFragment)
)
}
}
}
popBackStack() 返回的是 Boolean,表示是否成功弹出。如果返回 false,说明已经到栈底了。我在项目中遇到过连续调用 popBackStack() 导致 Fragment 重叠的问题,所以一定要检查返回值。
14.5 测试导航参数传递:数据丢了怎么办?
导航参数传递是 Navigation 的核心功能之一。你从 A Fragment 跳到 B Fragment,总得带点数据过去吧?比如用户 ID、商品详情等等。如果参数传丢了,或者类型不对,那 B Fragment 拿到的就是 null,直接崩。
测试参数传递,核心是验证 NavArgs 是否正确解析。
@RunWith(AndroidJUnit4::class)
class NavigationArgsTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testNavigateWithStringArg() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// 构造参数
val args = Bundle().apply {
putString("userId", "12345")
}
// 导航并传递参数
navController.navigate(R.id.action_home_to_detail, args)
// 验证参数传递成功
val currentDestination = navController.currentDestination
val arguments = currentDestination?.arguments ?: return@onActivity
val userId = arguments.getString("userId")
assertThat(userId, `is`("12345"))
}
}
@Test
fun testNavigateWithSafeArgs() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// 使用 Safe Args 传递参数
val action = HomeFragmentDirections.actionHomeToDetail("12345")
navController.navigate(action)
// 验证参数
val currentDestination = navController.currentDestination
val arguments = currentDestination?.arguments ?: return@onActivity
val userId = arguments.getString("userId")
assertThat(userId, `is`("12345"))
}
}
@Test
fun testNavigateWithMissingArg() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// 不传参数直接导航
try {
navController.navigate(R.id.action_home_to_detail)
// 如果没抛异常,说明参数是可选的
// 这里可以根据实际情况断言
} catch (e: IllegalArgumentException) {
// 如果参数是必填的,会抛异常
assertThat(e.message, containsString("userId"))
}
}
}
}
14.6 综合测试:模拟完整导航流程
上面我们拆开测了每个环节,但实际项目中,导航是一个完整的流程。我习惯写一个综合测试,模拟用户从 A 到 B 再到 C,然后返回,同时验证参数和返回栈。
@RunWith(AndroidJUnit4::class)
class NavigationIntegrationTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testFullNavigationFlow() {
val scenario = activityRule.scenario
scenario.onActivity { activity ->
val navController = activity.findNavController(R.id.nav_host_fragment)
// 1. 从 Home 跳到 Detail,带参数
val detailArgs = Bundle().apply {
putString("itemId", "item_001")
}
navController.navigate(R.id.action_home_to_detail, detailArgs)
// 验证在 Detail 页面,参数正确
assertThat(navController.currentDestination?.id, `is`(R.id.detailFragment))
val detailDest = navController.currentDestination
assertThat(detailDest?.arguments?.getString("itemId"), `is`("item_001"))
// 2. 从 Detail 跳到 Settings
navController.navigate(R.id.action_detail_to_settings)
assertThat(navController.currentDestination?.id, `is`(R.id.settingsFragment))
// 3. 返回两次,回到 Home
navController.popBackStack() // 回到 Detail
navController.popBackStack() // 回到 Home
assertThat(navController.currentDestination?.id, `is`(R.id.homeFragment))
// 4. 验证返回栈只有 Home
assertThat(navController.backStack.size, `is`(1))
}
}
}
- 导航图测试:验证 XML 结构合法,action ID 不重复
- NavHost 测试:验证 Fragment 被正确加载
- DeepLink 测试:验证外部跳转能到达正确目的地
- 返回栈测试:验证
popBackStack()行为符合预期 - 参数传递测试:验证
NavArgs正确解析
把这些测试写全了,Navigation 组件基本就不会出大问题。嗯,剩下的就是业务逻辑的测试了,那是另一回事。
公众号:蓝海资料掘金营,微信deep3321