如何在Robolectric单元测试中为Dagger2注入的Jetpack ViewModel提供Mock实例?
嘿,我来帮你搞定这个Robolectric测试里Mock ViewModel的问题!你的场景是Activity通过Dagger注入的ViewModelProvider.Factory来获取ViewModel,要做单元测试的话,核心就是把这个Factory替换成咱们自己的Mock版本,让它返回预先定义好的Mock ViewModel。下面给你两种实用的实现方法,你可以根据需求选:
方法一:用反射直接替换Activity里的Factory(快速简单)
这种方法不需要改Dagger的配置,直接在测试里用反射把Activity的vmFactory换成Mock的,适合单个测试场景快速验证。
步骤1:准备Mock对象
用Mockito(或者MockK,看你习惯)创建ActivityViewModel和ViewModelProvider.Factory的Mock实例,指定Factory的create方法返回我们的Mock ViewModel:
@RunWith(RobolectricTestRunner::class) class ActivityEpisodeListTest { @Mock private lateinit var mockViewModel: ActivityViewModel @Mock private lateinit var mockVmFactory: ViewModelProvider.Factory @Before fun setup() { // 初始化Mockito MockitoAnnotations.openMocks(this) // 让Mock Factory返回我们的Mock ViewModel `when`(mockVmFactory.create(ActivityViewModel::class.java)) .thenReturn(mockViewModel) }
步骤2:替换Activity的Factory并启动
Robolectric创建Activity时,默认会调用onCreate,所以咱们要先拿到Activity实例,替换vmFactory字段,再手动调用onCreate:
@Test fun `activity should use mock view model from factory`() { // 创建Activity实例(还没调用onCreate) val activity = Robolectric.buildActivity(ActivityEpisodeList::class.java).get() // 用反射替换vmFactory字段 val vmFactoryField = ActivityEpisodeList::class.java.getDeclaredField("vmFactory") vmFactoryField.isAccessible = true vmFactoryField.set(activity, mockVmFactory) // 手动调用onCreate,让Activity初始化ViewModel activity.onCreate(null) // 验证Activity里的ViewModel确实是咱们的Mock实例 val vmField = ActivityEpisodeList::class.java.getDeclaredField("vm") vmField.isAccessible = true val activityVm = vmField.get(activity) as ActivityViewModel assertThat(activityVm).isEqualTo(mockViewModel) // 这里可以继续验证业务逻辑,比如如果Activity在onCreate里调用了vm.loadEpisodes(),可以加: // verify(mockViewModel).loadEpisodes() } }
方法二:用Dagger测试组件替换依赖(更贴近真实注入)
如果你的测试需要多个依赖替换,或者想更贴近真实的注入流程,可以创建一个测试用的Dagger组件,绑定Mock ViewModel。
步骤1:创建测试Module和Component
写一个Test版本的ViewModelModule,绑定Mock的ActivityViewModel,再创建对应的Test AppComponent:
// 测试用的ViewModelModule,绑定Mock ViewModel @Module abstract class TestViewModelModule { @Binds internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory @Binds @IntoMap @ViewModelKey(ActivityViewModel::class) internal abstract fun postListViewModel(viewModel: ActivityViewModel): ViewModel } // 测试用的AppComponent,继承自原来的AppComponent @Singleton @Component(modules = [TestViewModelModule::class]) interface TestAppComponent : AppComponent { fun inject(test: ActivityEpisodeListTest) } // 测试用的Application类,返回TestAppComponent class TestMvvmDaggerApp : MvvmDaggerApp() { override fun createAppComponent(): AppComponent { return DaggerTestAppComponent.builder() .viewModelModule(TestViewModelModule()) .build() } }
步骤2:编写测试用例
在测试里指定用Test Application,然后通过Dagger注入Mock ViewModel,验证Activity使用的是这个实例:
@Config(application = TestMvvmDaggerApp::class) @RunWith(RobolectricTestRunner::class) class ActivityEpisodeListTest { @Inject lateinit var mockViewModel: ActivityViewModel @Before fun setup() { // 从Test Application获取Component并注入测试类 val testApp = ApplicationProvider.getApplicationContext<TestMvvmDaggerApp>() testApp.appComponent.inject(this) } @Test fun `activity should use injected mock view model`() { // 启动Activity,Robolectric会自动调用onCreate并完成注入 val activity = Robolectric.buildActivity(ActivityEpisodeList::class.java) .setup() .get() // 验证Activity的ViewModel和我们注入的Mock实例一致 val vmField = ActivityEpisodeList::class.java.getDeclaredField("vm") vmField.isAccessible = true val activityVm = vmField.get(activity) as ActivityViewModel assertThat(activityVm).isEqualTo(mockViewModel) } }
两种方法对比
- 方法一:快速上手,不需要修改Dagger配置,适合单个测试场景或者简单验证。缺点是用了反射,代码稍微有点“hack”感。
- 方法二:更贴近真实的依赖注入流程,适合复杂测试场景,多个测试用例可以复用Test Component。缺点是需要额外编写测试Module和Component,代码量稍大。
小贴士
- 如果你用MockK代替Mockito,代码会更简洁,比如创建Mock对象只需
val mockViewModel = mockk<ActivityViewModel>(),不需要@Mock注解和MockitoAnnotations.openMocks(this)。 - 确保Robolectric版本和你的AndroidX、Dagger版本兼容,避免出现奇怪的兼容性问题。
内容的提问来源于stack exchange,提问作者SpecialSnowflake

