You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Jetpack Compose导航的跨图路由传参及深层链接测试?

Jetpack Compose导航测试的参数依赖解决方案

问题背景

我需要测试Jetpack Compose导航的跨图/路由数据传递、深层链接功能,计划覆盖以下场景:

  • 从aScreen启动应用
  • 导航至带参数的bScreen,验证其ViewModel的param1值为abc
  • 导航至带多参数的cScreen

但编写NavigationTest时,创建MainNavHost需要onBackClick、AppState、DeviceDisplayState等多个参数,其中AppState依赖的NetworkMonitor是Activity注入对象,DeviceDisplayState的构建逻辑在测试环境中也难以实现,特此寻求解决方案。

现有测试代码

class NavigationTest {
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeTestRule = createComposeRule()

    lateinit var navController: TestNavHostController

    @OptIn(ExperimentalMaterialNavigationApi::class)
    @Before
    fun setupAppNavHost() {
        hiltRule.inject()

        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            MainNavHost(navController = navController) // 此处缺少必要参数
        }
    }

    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}
@Composable
fun MainNavHost(
    onBackClick: () -> Unit,
    appState: AppState,
    deviceDisplayState: DeviceDisplayState,
    bottomSheetNavigator: BottomSheetNavigator,
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
    startDestination: String = entryGraphRoutePattern
) {
    ModalBottomSheetLayout(
        bottomSheetNavigator,
        modifier = modifier.fillMaxSize(),
        sheetShape = RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp)
    ) {
        AnimatedNavHost(
            navController = navController,
            startDestination = startDestination,
            enterTransition = { EnterTransition.None },
            exitTransition = { ExitTransition.None },
            modifier = modifier
        ) {
           // 导航图定义
        }
    }
}
@OptIn(ExperimentalMaterialNavigationApi::class)
@Stable
class AppState constructor(
    val bottomSheetNavigator: BottomSheetNavigator,
    val navController: NavHostController,
    val webViewState: WebViewState,
    coroutineScope: CoroutineScope,
    networkMonitor: NetworkMonitor
) {
   // AppState逻辑
}
// Activity中注入NetworkMonitor
@Inject
lateinit var networkMonitor: NetworkMonitor
// DeviceDisplayState构建逻辑
// rememberDeviceDisplayState内部封装了对DeviceDisplayState的remember处理
val deviceDisplayState = rememberDeviceDisplayState(
                displays = displayManager?.displays,
                presentationHandler = this,
                forceSplit = true
               )

解决方案

针对每个依赖参数,我们可以通过**测试替身(Test Double)**或测试环境可用的实例来构建:

1. 处理onBackClick参数

测试中不需要真实的返回逻辑,直接传递空实现的Lambda即可:

onBackClick = {}

2. 构建测试用的AppState

为AppState的依赖提供测试替身,避免依赖真实的生产环境对象:

  • 首先定义NetworkMonitor的测试替身:
class FakeNetworkMonitor : NetworkMonitor {
    // 按测试需求返回固定状态,比如默认联网
    override val isOnline: StateFlow<Boolean> = MutableStateFlow(true)
    // 其他接口方法做空实现或固定返回
}
  • 然后在测试中构建AppState:
val bottomSheetNavigator = BottomSheetNavigator()
val testScope = CoroutineScope(Dispatchers.Unconfined)
val fakeNetworkMonitor = FakeNetworkMonitor()
val fakeWebViewState = WebViewState() // 若测试不需要WebView逻辑,直接创建空实例

val appState = AppState(
    bottomSheetNavigator = bottomSheetNavigator,
    navController = navController,
    webViewState = fakeWebViewState,
    coroutineScope = testScope,
    networkMonitor = fakeNetworkMonitor
)

3. 构建测试用的DeviceDisplayState

从测试环境的Context中获取DisplayManager,并使用主线程Handler作为presentationHandler:

val context = LocalContext.current
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val deviceDisplayState = rememberDeviceDisplayState(
    displays = displayManager.displays,
    presentationHandler = Handler(Looper.getMainLooper()),
    forceSplit = false // 按测试需求设置值
)

4. 整合所有参数到测试中

修改setupAppNavHost方法,补全所有必要参数:

@OptIn(ExperimentalMaterialNavigationApi::class)
@Before
fun setupAppNavHost() {
    hiltRule.inject()

    composeTestRule.setContent {
        navController = TestNavHostController(LocalContext.current).apply {
            navigatorProvider.addNavigator(ComposeNavigator())
            navigatorProvider.addNavigator(BottomSheetNavigator()) // 必须添加BottomSheet导航器
        }

        // 构建BottomSheetNavigator
        val bottomSheetNavigator = BottomSheetNavigator()
        // 构建AppState依赖
        val testScope = CoroutineScope(Dispatchers.Unconfined)
        val fakeNetworkMonitor = FakeNetworkMonitor()
        val fakeWebViewState = WebViewState()
        val appState = AppState(
            bottomSheetNavigator = bottomSheetNavigator,
            navController = navController,
            webViewState = fakeWebViewState,
            coroutineScope = testScope,
            networkMonitor = fakeNetworkMonitor
        )

        // 构建DeviceDisplayState
        val context = LocalContext.current
        val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        val deviceDisplayState = rememberDeviceDisplayState(
            displays = displayManager.displays,
            presentationHandler = Handler(Looper.getMainLooper()),
            forceSplit = false
        )

        // 初始化MainNavHost
        MainNavHost(
            onBackClick = {},
            appState = appState,
            deviceDisplayState = deviceDisplayState,
            bottomSheetNavigator = bottomSheetNavigator,
            navController = navController
        )
    }
}

额外优化建议

  • 若依赖过多,可通过Hilt的@TestInstallIn注解替换生产环境的依赖绑定,在测试中自动注入测试替身
  • 验证ViewModel参数时,可通过composeTestRule.runOnUiThread获取目标ViewModel,或直接在测试中注入测试用ViewModel
  • 测试深层链接时,使用navController.navigate(Uri.parse("your-deep-link-uri"))触发导航,再验证目标页面是否显示

内容的提问来源于stack exchange,提问作者c-an

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 07:27:03