Oleh Liskovych
Oleh Liskovych

Reputation: 1061

How to provide test/mock dependencies for Dagger2 @Subcomponent?

In different tutorials I see how to substitute @Component`s dependencies with mock or fakes. To do so, one can make test variant of @Component extending the regular version. But I haven't found how to do the same for @Subcomponent.

Here is my setup. Component:

@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
    fun plus(userModule: UserModule): UserComponent
    ...
}

Test version of Component:

@Singleton
@Component(modules = [TestAppModule::class])
interface TestAppComponent: AppComponent

Subcomponent:

@UserScope
@Subcomponent(modules = [UserModule::class])
interface UserComponent

Usage:

@Before
fun setUp() {
    MockKAnnotations.init(this)

    val loginManagerMock = mockk<LoginManager>()

    val testAppModule = TestAppModule(
        context = app,
        loginManager = loginManagerMock
    )

    val appComponent = DaggerTestAppComponent.builder()
        .testAppModule(testAppModule)
        .build()

    val testUserModule = TestUserModule(
        context = app,
        userEmail = "[email protected]",
        pdaRepository = pdaRepositoryMock
    )

    val userComponent = appComponent.plus(testUserModule) // this is not working

    every { loginManagerMock.userComponent } returns userComponent

    app.appComponent = appComponent

}

The problem is that I can't instantiate @Subcomponent in the same way I instantiate @Component. I have to use plus(userModule: UserModule): UserComponent method of AppComponent. It's not possible for TestAppModule to extend AppModule and override @Provides methods.

I would be grateful if someone could tell how to substitute dependencies provided by @Subcomponent's UserModule with mocks or fakes?

Upvotes: 0

Views: 600

Answers (2)

Oleh Liskovych
Oleh Liskovych

Reputation: 1061

Based on Jeff Bowman's second advice.

Test version of AppComponent:

@Singleton
@Component(modules = [MockAppModule::class])
interface MockAppComponent: AppComponent {

    fun mockUserComponent(): MockUserComponent.Factory

    @Component.Factory
    interface Factory {
        fun create(
            @BindsInstance context: Context,
            @BindsInstance toaster: Toaster,
            @BindsInstance appConfigProvider: AppConfigProvider,
            @BindsInstance okHttpClient: OkHttpClient,
            @BindsInstance authorizationInterceptor: AuthorizationInterceptor,
            @BindsInstance loginRefresher: ILoginRefresher,
            @BindsInstance retrofit: Retrofit,
            @BindsInstance fleetApi: FleetApi,
            @BindsInstance gson: Gson,
            @BindsInstance @Named("mainPreferences") sharedPreferences: SharedPreferences,
            @BindsInstance analyticsTracker: AnalyticsTracker,
            @BindsInstance permissionsManager: PermissionsManager,
            @BindsInstance loginManager: LoginManager,
            @BindsInstance loginRepository: ILoginRepository,
            @BindsInstance filesRepository: IFilesRepository,
            @BindsInstance dispatchersProvider: IDispatchersProvider,
            @BindsInstance dataStoreProvider: DataStoreProvider
        ): MockAppComponent
    }

    companion object {
        fun createWith(
            context: Context,
            toaster: Toaster = mockk(),
            appConfigProvider: AppConfigProvider = mockk(),
            okHttpClient: OkHttpClient = mockk(),
            authorizationInterceptor: AuthorizationInterceptor = mockk(),
            loginRefresher: ILoginRefresher = mockk(),
            retrofit: Retrofit = mockk(),
            fleetApi: FleetApi = mockk(),
            gson: Gson = mockk(),
            @Named("mainPreferences") sharedPreferences: SharedPreferences = mockk(),
            analyticsTracker: AnalyticsTracker = mockk(),
            permissionsManager: PermissionsManager = mockk(),
            loginManager: LoginManager = mockk(),
            loginRepository: ILoginRepository = mockk(),
            filesRepository: IFilesRepository = mockk(),
            dispatchersProvider: IDispatchersProvider = mockk(),
            dataStoreProvider: DataStoreProvider = mockk()
        ): MockAppComponent = DaggerMockAppComponent.factory().create(
            context,
            toaster,
            appConfigProvider,
            okHttpClient,
            authorizationInterceptor,
            loginRefresher,
            retrofit,
            fleetApi,
            gson,
            sharedPreferences,
            analyticsTracker,
            permissionsManager,
            loginManager,
            loginRepository,
            filesRepository,
            dispatchersProvider,
            dataStoreProvider
        )
    }
}

Module for MockAppComponent:

@Module (subcomponents = [MockUserComponent::class])
class MockAppModule

Test version of UserComponent:

@UserScope
@Subcomponent
interface MockUserComponent: UserComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(
            @BindsInstance userSharedPreferences: SharedPreferences,
            @BindsInstance userConfigProvider: UserConfigProvider,
            @BindsInstance iTripsRepository: ITripsRepository,
            @BindsInstance iVehiclesRepository: IVehiclesRepository,
            @BindsInstance iDriversRepository: IDriversRepository
        ): MockUserComponent
    }

}

fun MockAppComponent.createUserComponentWith(
    userSharedPreferences: SharedPreferences = mockk(),
    userConfigProvider: UserConfigProvider = mockk(),
    iTripsRepository: ITripsRepository = mockk(),
    iVehiclesRepository: IVehiclesRepository = mockk(),
    iDriversRepository: IDriversRepository  = mockk()
): MockUserComponent = mockUserComponent().create(
    userSharedPreferences,
    userConfigProvider,
    iTripsRepository,
    iVehiclesRepository,
    iDriversRepository
)

Usage:

    @Before
    fun setUp() {
        viewModel = SomeViewModel()

        val app: App = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as App

        val loginManagerMock = mockk<LoginManager>()
        val appComponent = MockAppComponent.createWith(
            app,
            loginManager = loginManagerMock
        )

        val vehicleRepository = mockk<VehiclesRepository>()
        every { vehicleRepository.getDummyString() } returns "Surprising string"

        val userComponent = appComponent.createUserComponentWith(
            iVehiclesRepository = vehicleRepository
        )

        every { loginManagerMock.userComponent } returns userComponent
        app.appComponent = appComponent
        loginManagerMock.userComponent!!.inject(viewModel)

    }

Upvotes: 0

Jeff Bowman
Jeff Bowman

Reputation: 95704

One difficulty here is you are using the legacy method of subcomponent creation by including a subcomponent "factory method" on the Component itself. Through this, the specific subcomponent type is expressed directly through the component interface, making it more difficult to do the component subclassing you're talking about.

You have a few options:

Option 1: Subclass UserComponent to become TestUserComponent, and override TestAppComponent.plus to return TestUserComponent instead of Component. On one hand, your override will work because the return value is more specific than the method you're overloading: in subclasses or subinterfaces, for polymorphic compatibility, parameters can be equal or more general and return values can be equal or more specific. However, the passed set of modules needs to be the same for polymorphism's sake. You could split UserModule into three modules, the instantiable UserParameterModule, the production UserProductionModule, and the test-only UserTestModule that provides overrides, but that all could get pretty hairy pretty quickly.

Option 2: Specify UserComponent using Module.subcomponents instead of a plus method. Instead of exposing your plus method, expose the UserComponent.Builder or UserComponent.Factory you create to replace the plus method. (This would require refactoring some production code.) Though I haven't tried it, you may be able to bind UserComponent.Factory to a matching TestUserComponent.Factory, though like with Option 1 you'd still need to refactor your Modules because you're still trying to make the factory method work polymorphically.

Option 3: Because the modules differ, stop trying to make your production graph and test graph work exactly the same way. Instead, switch to Module.subcomponents as above and create a "UserComponentCreator" interface, which would also require refactoring production code but would allow you to interact with your own ProductionUserComponentCreator instance in prod and a TestUserComponentCreator in your test. This is the kind of abstraction and indirection that Java is infamous for, but it would mean that you retain a lot of control over what TestUserComponentCreator actually does for you.

Option 4: Use a different test seam, such as producing an entirely different Component on which to install your mock UserComponent with all of its AppComponent dependencies mocked, or supply a UserComponent created by Kotlin delegation instead of using your @Provides methods.

As a side note, proceed with caution when creating a Dagger graph out of a mix of real and fake components, since it can be much more difficult to establish which parts are real and which parts are fake. You wouldn't want to write a unit test that inadvertently tests your mock, while the actual system in production falls out of spec for lack of actual testing.

Upvotes: 1

Related Questions