Reputation: 1061
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
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
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