My Android production is code full of Hilt modules that install various production implementations:
@Module
@InstallIn(ApplicationComponent.class)
public abstract class TimeModule {...}
@Module
@InstallIn(ApplicationComponent.class)
public abstract class DatabaseModule {...}
In all my instrumented tests, I would like to replace those bindings with fakes. My test codebase includes modules that bind fake implementations, but having two modules provide the same class obviously causes compile-time errors.
The Hilt documentation recommends using @UninstallModule()
, but that means I'd have to add UninstallModule
for every single production module in every single test. That seems like the wrong approach.
How would one normally replace production modules with fake modules? And is there a way to install modules from another module like Guice does, so I could remove @InstallIn
from all my production modules and instead simply have one ProductionModule
that installs all the individual modules? That would make it easier to just uninstall one module in tests.
How would one normally replace production modules with fake modules?
Probably how it's normally done, is like the documentation said with the UninstallModule
annotation. But here is an alternative, which I like to use, using build flavors:
I like to organize my project, so there are mock
and live
flavors. And there are 3 folders inside my app module: src/main/kotlin
with Activities, Fragments etc..., src/mock/kotlin
where my fake bindings live, and finally src/live/kotlin
where my real production bindings live.
Here's the relevant config from my app
level build.gradle.kts
:
android {
productFlavors {
flavorDimensions("environment")
register("mock") {
dimension = "environment"
}
register("dev") {
dimension = "environment"
}
register("prod") {
dimension = "environment"
}
sourceSets {
getByName("mock").java.srcDir("src/mock/kotlin")
getByName("dev").java.srcDir("src/live/kotlin")
getByName("prod").java.srcDir("src/live/kotlin")
}
}
}
Inside the live InteractorModule
:
@Module
@InstallIn(ApplicationComponent::class)
abstract class InteractorModule {
@Binds
abstract fun bindTodoInteractor(interactor: TodoInteractorImpl): TodoInteractor
}
Inside the FakeInteractorsModule
:
@Module
@InstallIn(ApplicationComponent::class)
abstract class InteractorModule {
@Binds
abstract fun bindTodoInteractor(interactor: TodoInteractorFake): TodoInteractor
}
So now you can use the build variant tab to change between the real and the mock implementations of your interfaces. So if you want to use your fakes inside your instrumentation tests use the mock flavor while running the tests.
One upside of this method, is that by changing the build variant, you can swich between your instrumentation tests using your fakes to using the live implementations. Conversly, you can use your fake implementations inside the actuall application, which can be nice, if you just want to try out the app with mock data.
I hope this helped a bit to at least promote some ideas, on how you can solve this "two implementations for one interface" dilemma!