MVVM架构下Dagger2+Espresso+Mockito安卓UI测试搭建方案问询
我之前在做Dagger2+MVVM的Espresso UI测试时,也碰到过和你一模一样的DaggerMock报错问题。结合你的架构(自定义Scope、子组件、AndroidInjection),给你几个经过验证的可行方案:
方案1:自定义测试Dagger组件(最稳定可控)
这个方案是完全为测试环境搭建一套独立的Dagger组件体系,彻底隔离生产依赖,适合复杂架构的长期维护。
步骤说明:
- 编写测试专用的Application类,重写Dagger初始化逻辑
- 扩展现有
Components对象,添加测试环境的组件初始化方法 - 创建测试用的Component和Module,提供模拟依赖(用MockK/Mockito)
- 配置测试Runner,让测试进程使用测试Application
代码示例:
测试Application类
class TestApp : Application() { override fun initDagger() { Components.initializeForTesting(this) } }
扩展Components对象
object Components : ComponentFactory { private lateinit var sAppComponent: AppComponent private lateinit var sTestAppComponent: TestAppComponent // 新增测试环境初始化方法 fun initializeForTesting(app: TestApp) { sTestAppComponent = DaggerTestAppComponent.builder() .testApplicationModule(TestApplicationModule(app)) .build() } // 重写ComponentFactory方法,优先返回测试组件 override fun app(): AppComponent { return if (::sTestAppComponent.isInitialized) sTestAppComponent else sAppComponent } override fun authenticated(): AuthenticatedComponent { return if (::sTestAppComponent.isInitialized) { // 测试环境下返回自定义子组件 sTestAppComponent.authenticatedComponentBuilder() .testUserModule(TestUserModule(1L, User.Role.ADMIN)) .build() } else { // 保留生产环境逻辑 sAppComponent.authenticatedComponentBuilder() .userModule(UserModule(0L, User.Role.GUEST)) .build() } } }
测试用Component和Module
// 测试根组件,继承生产组件 @Singleton @Component(modules = [TestApplicationModule::class, AppActivitiesModule::class, AndroidInjectionModule::class, AndroidSupportInjectionModule::class]) interface TestAppComponent : AppComponent { override fun authenticatedComponentBuilder(): TestAuthenticatedComponent.Builder @Component.Builder interface Builder : AppComponent.Builder { fun testApplicationModule(module: TestApplicationModule): Builder override fun build(): TestAppComponent } } // 测试子组件,继承生产子组件 @AuthenticatedScope @Subcomponent(modules = [TestAuthenticatedModule::class, AuthenticatedActivitiesModule::class]) interface TestAuthenticatedComponent : AuthenticatedComponent { @Subcomponent.Builder interface Builder : AuthenticatedComponent.Builder { fun testUserModule(module: TestUserModule): Builder override fun build(): TestAuthenticatedComponent } } // 测试Module,提供模拟依赖 @Module class TestApplicationModule(app: Application) : AppModule(app) { @Provides @Singleton override fun provideAppLevelService(): AppLevelService { return mockk(relaxed = true) // 用MockK创建宽松模拟对象 } } @Module class TestUserModule(userId: Long, userRole: User.Role) : UserModule(userId, userRole) { @Provides @AuthenticatedScope override fun provideAuthenticatedLevelServices(): AuthenticatedLevelServices { return mockk(relaxed = true) } }
测试Runner配置
class TestRunner : AndroidJUnitRunner() { override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { // 强制使用测试Application return super.newApplication(cl, TestApp::class.java.name, context) } }
在build.gradle中指定测试Runner:
android { defaultConfig { testInstrumentationRunner "com.yourpackage.TestRunner" } }
方案2:修复DaggerMock的使用方式
你遇到的报错You must define overridden objects using a @Provides annotated method instead of using @Inject annotation,是因为DaggerMock无法直接处理自定义Scope(比如AuthenticatedScope)或子组件中的@Inject类,必须显式通过@Provides提供模拟对象。
代码示例:
@RunWith(DaggerMockJUnitRunner::class) @DaggerMock(component = AppComponent::class) class MyActivityTest { @get:Rule val activityRule = ActivityScenarioRule(MyActivity::class.java) // 测试专用Module,提供所有需要模拟的依赖 @Module class TestModule { @Provides @Singleton fun provideAppLevelService(): AppLevelService { return Mockito.mock(AppLevelService::class.java) } @Provides @AuthenticatedScope fun provideAuthenticatedLevelServices(): AuthenticatedLevelServices { return Mockito.mock(AuthenticatedLevelServices::class.java) } } // 扩展生产组件,引入测试Module @Component(modules = [TestModule::class, AndroidInjectionModule::class, AndroidSupportInjectionModule::class]) interface TestComponent : AppComponent { // 确保能注入测试目标Activity fun inject(activity: MyActivity) // 重写子组件Builder,引入测试Module override fun authenticatedComponentBuilder(): TestAuthenticatedComponent.Builder } @AuthenticatedScope @Subcomponent(modules = [TestModule::class]) interface TestAuthenticatedComponent : AuthenticatedComponent @Test fun testActivityInitialization() { // 模拟服务行为 val mockAppService = Mockito.mock(AppLevelService::class.java) Mockito.`when`(mockAppService.getSomeData()).thenReturn("Test Data") // 执行Espresso断言 onView(withId(R.id.data_text)).check(matches(withText("Test Data"))) } }
注意:确保使用最新版本的DaggerMock,旧版本对自定义Scope的支持存在bug。
方案3:手动注入模拟依赖(快速验证场景)
如果只是想快速验证单个Activity的UI逻辑,不想改动Dagger组件,可以通过反射手动替换ViewModel的依赖。
代码示例:
class MyActivityTest { @get:Rule val activityRule = ActivityScenarioRule(MyActivity::class.java) private val mockAppService = mockk<AppLevelService>(relaxed = true) private val mockAuthService = mockk<AuthenticatedLevelServices>(relaxed = true) @Before fun setup() { activityRule.scenario.onActivity { activity -> // 创建带模拟依赖的ViewModel val testViewModel = ViewModel(mockAppService, mockAuthService) // 反射设置Activity中的vmProvider val field = MyActivity::class.java.getDeclaredField("vmProvider") field.isAccessible = true field.set(activity, Provide { testViewModel }) } } @Test fun testButtonClick() { // 模拟服务返回结果 MockK.coEvery { mockAppService.fetchData() } returns "Clicked!" // 执行点击操作 onView(withId(R.id.action_button)).perform(click()) // 断言UI变化 onView(withId(R.id.result_text)).check(matches(withText("Clicked!"))) } }
这个方案适合小范围快速测试,但扩展性差,不适合大型项目长期维护。
内容的提问来源于stack exchange,提问作者tasegula
相关产品推荐
相关产品推荐

