如何实现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() } }
MainNavHost及依赖定义
@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
相关产品推荐
相关产品推荐

