AndroidTest中覆盖Composable:解决FavouriteButton的Hilt ViewModel问题
问题背景
布局层级结构如下:
- SearchResultScreen
- ProductGridView
--- 以下组件由其他团队维护 --- - CustomProductTile
- InternalProductTile
- ProductTileImage
- FavouriteButton
核心问题:FavouriteButton内部通过Hilt直接创建ViewModel(未通过上层组件传递),导致编写SearchResultScreen的Android测试时,Hilt无法实例化该ViewModel,抛出异常。
已尝试的方案及痛点:
- 使用
HiltAndroidRule注入依赖:需要Mock大量关联模块的无关依赖,测试成本极高; - 手动创建带Mock仓库的ViewModel并逐层传递:需要修改所有下层Composable的参数或Lambda,改动范围过大,无法快速落地。
约束条件:无法快速对ProductGridView以下的Composable进行大幅修改,已知底层组件自行创建ViewModel的设计不合理,但暂无法修正。
可行替代方案
方案1:利用Hilt测试绑定替换ViewModel实例
无需修改任何业务代码,仅通过Hilt的测试模块配置,为测试环境提供Mock的ViewModel,覆盖生产环境的注入逻辑:
- 创建测试专用Hilt模块(如果生产环境有对应的ViewModel提供模块,直接替换):
@Module @TestInstallIn( components = [SingletonComponent::class], replaces = [FavouriteViewModelProductionModule::class] // 替换生产环境的ViewModel模块 ) object TestFavouriteViewModelModule { @Provides fun provideMockFavouriteViewModel(): FavouriteViewModel { return mockk(relaxed = true) // 可根据测试需求配置具体Mock行为 } }
- 在测试类中绑定Mock实例(如果使用
@HiltViewModel注解,可直接用@BindValue简化):
@HiltAndroidTest class SearchResultScreenTest { @get:Rule val hiltRule = HiltAndroidRule(this) @BindValue val mockFavouriteViewModel: FavouriteViewModel = mockk(relaxed = true) @Before fun setUp() { hiltRule.inject() } @Test fun testSearchResultScreen() { composeTestRule.setContent { YourAppTheme { SearchResultScreen() } } // 执行测试断言逻辑 } }
这种方式完全无需改动业务组件,仅通过测试配置让Hilt在测试时注入Mock ViewModel,直接解决实例化报错问题。
方案2:用CompositionLocal封装ViewModel获取逻辑(极小改动)
如果可以说服其他团队对FavouriteButton做几行代码的微小改动(无业务逻辑影响),可以用Compose的CompositionLocal实现ViewModel的上层注入,测试时替换实例:
- 定义CompositionLocal:
val LocalFavouriteViewModel = compositionLocalOf<FavouriteViewModel> { error("FavouriteViewModel未提供,请在上层组件中配置") }
- 修改FavouriteButton:将原有的
hiltViewModel()调用改为从CompositionLocal获取:
@Composable fun FavouriteButton() { val viewModel = LocalFavouriteViewModel.current // 原有业务逻辑完全保留 }
- 生产环境上层提供真实实例:在
ProductGridView中注入真实ViewModel并传递:
@Composable fun ProductGridView() { val favouriteViewModel = hiltViewModel<FavouriteViewModel>() CompositionLocalProvider(LocalFavouriteViewModel provides favouriteViewModel) { // 原有布局逻辑不变 } }
- 测试环境提供Mock实例:
@Test fun testSearchResultScreen() { val mockFavouriteViewModel = mockk<FavouriteViewModel>(relaxed = true) composeTestRule.setContent { CompositionLocalProvider(LocalFavouriteViewModel provides mockFavouriteViewModel) { SearchResultScreen() } } // 执行测试断言 }
该方案改动极小,仅修改FavouriteButton的ViewModel获取方式,对原有功能无影响,同时彻底解决测试依赖问题。
方案3:Mock ViewModel创建函数(备选)
如果不想依赖Hilt测试规则,可直接用mockk mock掉hiltViewModel函数的返回值:
@Test fun testSearchResultScreen() { val mockViewModel = mockk<FavouriteViewModel>(relaxed = true) // Mock hiltViewModel的调用返回Mock实例 coEvery { any<ComposableContext>().hiltViewModel<FavouriteViewModel>() } returns mockViewModel composeTestRule.setContent { SearchResultScreen() } // 测试断言逻辑 }
注意:这种方式需要确保mockk能正确拦截ViewModel的创建逻辑,部分场景下可能受Hilt内部实现影响,稳定性略差于前两种方案。
方案优先级推荐
优先选择方案1,无需改动业务代码,仅通过测试配置即可解决问题,成本最低;若方案1因Hilt模块结构限制无法实现,选择方案2,改动极小且对业务无侵入;方案3作为备选场景使用。
内容的提问来源于stack exchange,提问作者JustSightseeing

