11、导航测试:NavHost测试、路由跳转验证、返回栈测试

导航测试,说白了就是验证你的App页面跳转对不对。

我见过太多项目,功能测试都过了,结果上线后发现某个深层页面回退时直接闪退。嗯,这就是导航测试没做到位。

今天我们就来聊聊Compose里的导航测试怎么做。我会从NavHost测试讲起,再到路由跳转验证,最后说返回栈测试。这些都是我在实际项目中踩过的坑,希望能帮你少走弯路。

11.1 NavHost测试基础

先说说NavHost怎么测。你想想看,NavHost是整个导航系统的核心,它决定了哪个页面该显示。测试它,其实就是验证路由和页面的对应关系。

我习惯用这种方式写NavHost:

// 定义路由常量
object Routes {
    const val HOME = "home"
    const val DETAIL = "detail/{id}"
    const val SETTINGS = "settings"
}

// 导航图
@Composable
fun AppNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Routes.HOME,
        modifier = modifier
    ) {
        composable(Routes.HOME) {
            HomeScreen(
                onNavigateToDetail = { id ->
                    navController.navigate("detail/$id")
                }
            )
        }
        composable(
            route = Routes.DETAIL,
            arguments = listOf(
                navArgument("id") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            val id = backStackEntry.arguments?.getString("id") ?: ""
            DetailScreen(
                id = id,
                onBack = { navController.popBackStack() }
            )
        }
        composable(Routes.SETTINGS) {
            SettingsScreen(
                onBack = { navController.popBackStack() }
            )
        }
    }
}

测试时,我们需要创建一个NavController,然后渲染NavHost。这里有个关键点——要用TestNavHostController

@Test
fun testNavHost_rendersHomeScreen() {
    // 创建测试用的NavController
    val navController = TestNavHostController(LocalContext.current)
    
    // 设置Compose测试规则
    composeTestRule.setContent {
        AppNavHost(navController = navController)
    }
    
    // 验证首页内容显示
    composeTestRule.onNodeWithText("首页").assertIsDisplayed()
    composeTestRule.onNodeWithText("进入详情").assertIsDisplayed()
}
我的习惯:测试NavHost时,我会把NavController作为参数传进去,而不是在内部创建。这样测试时就能注入TestNavHostController,方便验证导航状态。

11.2 路由跳转验证

路由跳转验证,说白了就是检查点击按钮后,页面有没有正确跳转。

我曾在项目中遇到过一个问题:某个按钮点击后,页面跳转了,但参数传错了。结果详情页一直显示空数据,排查了半天才发现是路由参数没拼对。

所以,验证路由跳转要关注两点:

  • 跳转是否发生——检查当前路由是否正确
  • 参数是否传递正确——检查路由中的参数值

来看一个完整的测试示例:

@Test
fun testNavigateToDetail_withCorrectId() {
    val navController = TestNavHostController(LocalContext.current)
    
    composeTestRule.setContent {
        AppNavHost(navController = navController)
    }
    
    // 点击"进入详情"按钮
    composeTestRule.onNodeWithText("进入详情").performClick()
    
    // 验证路由是否正确
    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals("detail/{id}", route)
    
    // 验证参数是否正确
    val arguments = navController.currentBackStackEntry?.arguments
    assertEquals("123", arguments?.getString("id"))
}
核心要点:使用navController.currentBackStackEntry可以获取当前路由和参数。这是验证跳转是否正确的标准做法。

还有一种情况——深层链接跳转。比如从通知栏点击后,直接跳转到某个详情页。这种跳转也要测:

@Test
fun testDeepLinkNavigation() {
    val navController = TestNavHostController(LocalContext.current)
    
    composeTestRule.setContent {
        AppNavHost(navController = navController)
    }
    
    // 模拟深层链接跳转
    navController.navigate("detail/456")
    
    // 验证跳转结果
    composeTestRule.onNodeWithText("详情页").assertIsDisplayed()
    composeTestRule.onNodeWithText("ID: 456").assertIsDisplayed()
}

11.3 返回栈测试

返回栈测试,这是最容易出问题的地方。

我曾经遇到过一个线上bug:用户从首页进入详情页,再从详情页进入编辑页,然后点击返回,结果直接回到了首页。按理说应该先回到详情页才对。这就是返回栈管理出了问题。

返回栈测试主要验证三点:

  • 正常返回——点击返回按钮,回到上一页
  • 返回栈清空——某些场景下需要清空返回栈(比如登录后)
  • 多次返回——连续返回多个页面,验证栈的正确性

来看一个返回栈测试的例子:

@Test
fun testBackStack_popToPreviousScreen() {
    val navController = TestNavHostController(LocalContext.current)
    
    composeTestRule.setContent {
        AppNavHost(navController = navController)
    }
    
    // 先跳转到详情页
    composeTestRule.onNodeWithText("进入详情").performClick()
    composeTestRule.onNodeWithText("详情页").assertIsDisplayed()
    
    // 再跳转到编辑页
    composeTestRule.onNodeWithText("编辑").performClick()
    composeTestRule.onNodeWithText("编辑页").assertIsDisplayed()
    
    // 点击返回
    composeTestRule.onNodeWithText("返回").performClick()
    
    // 验证回到了详情页,而不是首页
    composeTestRule.onNodeWithText("详情页").assertIsDisplayed()
    composeTestRule.onNodeWithText("首页").assertDoesNotExist()
}
注意:测试返回栈时,一定要模拟完整的导航路径。只测一次跳转和返回是不够的,因为返回栈的问题往往在多次跳转后才暴露。

还有一种常见场景——登录后清空返回栈。比如用户从首页进入登录页,登录成功后应该直接跳转到主页,并且不能返回到登录页:

@Test
fun testLogin_clearsBackStack() {
    val navController = TestNavHostController(LocalContext.current)
    
    composeTestRule.setContent {
        AppNavHost(navController = navController)
    }
    
    // 模拟从首页跳转到登录页
    composeTestRule.onNodeWithText("登录").performClick()
    composeTestRule.onNodeWithText("登录页").assertIsDisplayed()
    
    // 模拟登录成功
    composeTestRule.onNodeWithText("登录成功").performClick()
    
    // 验证跳转到主页
    composeTestRule.onNodeWithText("主页").assertIsDisplayed()
    
    // 验证返回栈被清空(按返回键不会回到登录页)
    composeTestRule.onNodeWithText("返回").performClick()
    composeTestRule.onNodeWithText("主页").assertIsDisplayed()
}

11.4 导航测试的SVG流程图

下面这张图展示了导航测试的核心流程:

导航测试核心流程 NavHost测试 验证路由与页面对应 路由跳转验证 检查路由与参数正确性 返回栈测试 验证页面回退逻辑 测试要点 TestNavHostController 注入测试用NavController currentBackStackEntry 获取当前路由与参数 popBackStack() 验证返回栈行为 覆盖正常跳转、参数传递、返回栈清空三大场景

11.5 常见问题与避坑指南

最后,分享几个我在项目中遇到的坑:

我曾经踩过的坑:
  • 忘记设置startDestination——NavHost必须指定startDestination,否则会崩溃。测试时也要确保这个值正确。
  • 参数类型不匹配——路由中定义的是String,但传了Int。测试时一定要验证参数类型。
  • 返回栈溢出——连续跳转太多页面,没有及时清理返回栈。建议在测试中模拟多次跳转,验证栈的深度。
我的建议:写导航测试时,先画一张导航图,把所有的路由和跳转关系列出来。然后针对每条路径写测试用例。这样能保证覆盖全面,不会漏掉边界情况。

导航测试看似简单,但实际做起来有很多细节。记住一个原则:用户能走到的页面,测试都要覆盖到。特别是返回栈,这是用户最常操作的功能,也是最容易出问题的地方。

好了,导航测试就聊到这里。希望这些经验对你有帮助。


公众号:蓝海资料掘金营,微信deep3321