question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

`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

See original GitHub issue

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

  1. We have a dagger2 graph being created in the Application.attachBaseContext method.
override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        earlyInitComponent = DaggerEarlyInitComponent.factory().create()
  1. 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()
    }
  1. 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.)

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
techereticcommented, Oct 8, 2021

That fixed it!

Thanks!

I’ve updated the Example project with the changes that fixed it. See commit here https://github.com/techeretic/HiltRobolectricIssue/commit/22ff8e9534fe21f05126bd808779a18bf6af5a15

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

0reactions
techereticcommented, Oct 8, 2021

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Hilt testing guide | Android Developers
To replace the AnalyticsService binding in tests, create a new Hilt module in the test or androidTest folder with the fake dependency and...
Read more >
android - Getting failed in providing HiltTestApplication as a ...
To provide the application context, you need to replace app: HiltTestApplication with @ApplicationContext app: Context because ...
Read more >
Hilt: Supporting module uninstallation across all tests #1923
From https://developer.android.com/training/dependency-injection/hilt-testing: This only replaces the binding for a single test class.
Read more >
Testing - Hilt - Dagger
Hilt makes testing easier by bringing the power of dependency injection to your Android tests. Hilt allows your tests to easily access Dagger...
Read more >
testDebugUnitTest NoClassDefFoundError for dynamic ...
When running the testDebugUnitTest task for a dynamic feature module, ... through Android Studio 3.2.1 does not work when class has certain dependencies....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found