`TestInstallIn` doesn't replace the dependencies when being used to replace dependencies in a `SingletonComponent` for robolectric tests running on a class in a dynamic feature module
techeretic opened this issue · 5 comments
So, we have a complicated dagger2 app structure along with multiple dynamic feature modules.
I have created a sample project : https://github.com/techeretic/HiltRobolectricIssue that explains this issue.
Before I dive into the details, let me give some background about the app
- We have a dagger2 graph being created in the
Application.attachBaseContext
method.
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
earlyInitComponent = DaggerEarlyInitComponent.factory().create()
- The dependencies from dagger graph created in step 1 are bound to the hilt SingletonComponent
@[Module InstallIn(SingletonComponent::class)]
object FirstModule {
@Provides
fun provideEarlyInitDependency(): EarlyInitDependency {
return MainApplication.getInstance().earlyInitComponent.getEarlyInitDependency()
}
- We define a
DynamicFeatureDependencies
EntryPoint which we'll used to share dependencies with the dagger component in the dynamic feature module
@[EntryPoint InstallIn(SingletonComponent::class)]
interface DynamicFeatureDependencies {
fun getSingletonInterface(): SingletonInterface
}
...
...
@[DFM Component(
dependencies = [DynamicFeatureDependencies::class],
modules = [DynamicFeatureModule::class]
)]
interface DynamicFeatureComponent {
Now, since robolectric tests aren't supported in a dyamic feature modules, we create a separate Android Library just for running the robolectric tests. This library depends on the app module and the dynamic feature module.
In robolectrictests module, we have configured a robolectric test runner to run with the a custom app (because we have some custom setups being done in our implementation of the Application class).
open class TestRobolectricTestRunner(cls: Class<*>) : RobolectricTestRunner(cls),
GlobalConfigProvider {
// We need it this way in our project
override fun buildGlobalConfig(): Config {
return Config.Builder()
// This is a little hokey but it allows us to set our Robolectric SDK version in a single place.
.setSdk(Config.Builder().setSdk(28).build().sdk[0])
.setApplication(TestApplication::class.java)
.build()
}
override fun get(): Config = Config.Builder().setSdk(28).build()
}
and TestApplication is
class TestApplication : MainApplication() {
lateinit var dynamicFeatureDependencies: DynamicFeatureDependencies
override fun onCreate() {
super.onCreate()
dynamicFeatureDependencies = EntryPoints.get(this, DynamicFeatureDependencies::class.java)
}
We have a SingletonModule
that provides an implementation of SingletonInterface
const val DEFAULT_VALUE = 10
class SingletonDependency : SingletonInterface {
override val someValue: Int
get() = DEFAULT_VALUE
For testing, we setup a test implementation of the SingletonInterface
for our robolectric test
@[Module TestInstallIn(
components = [SingletonComponent::class],
replaces = [SingletonModule::class]
)]
object TestSingletonModule {
@[Provides Singleton]
fun provideSingletonDependency(): SingletonInterface {
return object : SingletonInterface {
override val someValue: Int
get() = TEST_VALUE
override fun doSomething(context: Context) {
// No Op
}
}
}
}
const val TEST_VALUE = 100
Eventually, when we run the test as
@RunWith(TestRobolectricTestRunner::class)
class ExampleUnitTest {
@Inject
lateinit var singletonInterface: SingletonInterface
@Before
fun setUp() {
DaggerRobolectricDynamicFeatureComponent.factory()
.create(
EntryPoints.get(TestApplication.getInstance(), DynamicFeatureDependencies::class.java)
)
.inject(this)
}
@Test
fun addition_isCorrect() {
assertEquals(singletonInterface.someValue, TEST_VALUE)
}
}
It fails.
expected:<10> but was:<100>
Expected :10
Actual :100
<Click to see difference>
Thus, the TEST_VALUE
which is being set in TestSingletonModule
is never part of the hilt singleton dagger graph.
So, either this TestInstallIn
doesn't work when using custom application implementation (not the @CustomTestApplication) and EntryPoints
used for sharing dependencies with a Dagger2 graph in a dynamic feature module.
(Note: Code snippets here are from a project https://github.com/techeretic/HiltRobolectricIssue that mimics our app code.)
@TestInstallIn
only works with @HiltAndroidTest
, but since your TestApplication
extends your MainApplication
you are using it with @HiltAndroidApp
. The reason this works this way is that the way we distinguish test vs prod is with those annotations on the root (they generate different code in the Application
). You'll have to use the @CustomTestApplication
(https://dagger.dev/hilt/testing#custom-test-application) to write your test.
How do I use @CustomTestApplication
with our application implementation?
@CustomTestApplication
forces to be used on an interface which doesn't have the dagger graph that we need to be created in attachBaseContext
. This dagger graph feeds into a module that is used with the hilt singleton component.
@CustomTestApplication
takes in a base class as an argument to the annotation. So you can use @CustomTestApplication(MainApplication::class)
and the test application we generate will subclass your MainApplication
.
Actually, as written, you'll need to do a little refactoring, which is to separate the @HiltAndroidApp
part off of the MainApplication
since it is incompatible with @HiltAndroidTest
. So something like...
@HiltAndroidApp
class MainApplication : MainApplicationImpl
// This class has all of the current logic with your custom Dagger component
open class MainApplicationImpl : Application
@CustomTestApplication(MainApplicationImpl::class)
interface CustomTest
Then setup your tests to use the generated CustomTest_Application
Ah! Lemme try it out.
I was attempting
@CustomTestApplication(MainApplication::class)
interface CustomApplication
and ran into this
public abstract interface CustomApplication {
^
@CustomTestApplication value cannot be annotated with @HiltAndroidApp. Found: com.bug.hiltrobolectricissue.MainApplication
[Hilt] Processing did not complete. See error above for details.
FAILURE: Build failed with an exception.
Thanks for explaining that I have to separate it out.
That fixed it!
Thanks!
I've updated the Example project with the changes that fixed it. See commit here techeretic/HiltRobolectricIssue@22ff8e9
One thing to call out is that I had to move out all the field injection from the MainApplication
to the class that has the @HiltAndroidApp
. Though this can worked around.
I'll continue with our app migration to hilt from dagger 2