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

MVVM架构下Dagger2+Espresso+Mockito安卓UI测试搭建方案问询

我之前在做Dagger2+MVVM的Espresso UI测试时,也碰到过和你一模一样的DaggerMock报错问题。结合你的架构(自定义Scope、子组件、AndroidInjection),给你几个经过验证的可行方案:

方案1:自定义测试Dagger组件(最稳定可控)

这个方案是完全为测试环境搭建一套独立的Dagger组件体系,彻底隔离生产依赖,适合复杂架构的长期维护。

步骤说明:

  1. 编写测试专用的Application类,重写Dagger初始化逻辑
  2. 扩展现有Components对象,添加测试环境的组件初始化方法
  3. 创建测试用的Component和Module,提供模拟依赖(用MockK/Mockito)
  4. 配置测试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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 06:28:31