dagger: @CustomTestApplication value cannot be annotated with @HiltAndroidApp

I have an application that needs injection:

@HiltAndroidApp
class MyApplication: Application() {
    @Inject lateinit var printerFactory: PrinterFactory
    ...
}

Other code in the app uses application.printerFactory.

The printerFactory is provided by this:

@Module
@InstallIn(ApplicationComponent::class)
object ProdModule {
    @Provides
    fun providePrinterFactory(): PrinterFactory {
        return FactoryThatReturnsARealPrinter()
    }
}

Now, for instrumented tests, I need to override this with a test version:

@HiltAndroidTest
@UninstallModules(ProdModule::class)
@RunWith(AndroidJUnit4::class)
@LargeTest
class MainTest {
    @get:Rule(order = 0)
    var hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    var intentsTestRule = IntentsTestRule(MainActivity::class.java, false, false)

    // Test replacements for production stuff.
    @Module
    @InstallIn(ApplicationComponent::class)
    class TestModule {
        @Provides
        fun providePrinterFactory(): PrinterFactory {
            return FactoryThatReturnsATestPrinter()
        }
    }
    ...
}

And so I get this error:

java.lang.IllegalStateException: Hilt test, MainTest, cannot use a @HiltAndroidApp application but found MyApplication. To fix, configure the test to use HiltTestApplication or a custom Hilt test application generated with @CustomTestApplication.

Well, I can’t use HiltTestApplication because I need to use MyApplication, which has dependencies injected.

So I added this:

    @CustomTestApplication(MyApplication::class)
    interface HiltTestApplication

And so I get this error:

error: [Hilt]
    public static abstract interface HiltTestApplication {
                           ^
  @CustomTestApplication value cannot be annotated with @HiltAndroidApp. Found: MyApplication

If MyApplication cannot be annotated with @HiltAndroidApp, then how is it expected to inject things?

Instructions unclear, ended up in inconsistent state.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 7
  • Comments: 25 (3 by maintainers)

Commits related to this issue

Most upvoted comments

Bad Solution Regarding the this documentation I create interface and put my test application:

@CustomTestApplication(TestApplication::class)
interface AndroidTestApplication

But it gives me exception:

Caused by: java.lang.InstantiationException: java.lang.Class<com.myapp.AndroidTestApplication> cannot be instantiated
        at java.lang.Class.newInstance(Native Method)
        at android.app.Instrumentation.newApplication(Instrumentation.java:1165)
        at com.myapp..AndroidTestRunner.newApplication(AndroidTestRunner.kt:16)
        at android.app.LoadedApk.makeApplication(LoadedApk.java:1218)

So i’m directly passing the generated hilt application class for my test app and skip using AndroidTestApplication interface and it works: Instrumentation.newApplication(AndroidTestApplication_Application::class.java, context)

@danysantiago I got the following issue when I use a base app. Do I missing something else ?

java.lang.RuntimeException: Unable to instantiate application com.sample.MainApplication: java.lang.InstantiationException: java.lang.Class<com.sample.di.TestApplication> cannot be instantiated

@CustomTestApplication(BaseApplication::class)
interface TestApplication
open class BaseApplication : Application()
@HiltAndroidApp
open class MainApplication : BaseApplication()

For these cases, we are working on an escape hatch to make it possible to use entry points from Applicaiton#onCreate() for special entry points.

@bcorso are you referring to EarlyEntryPoints?

Instead, try moving all application state into the Hilt SingletonComponent.

What about the scenario where you need to @Inject some fields into the Application but those fields are also invoked within Application#onCreate as part of initialization logic? For example, a Set<LifecycleObserver> where in onCreate(), you wish to add them to the ProcessLifecycleOwner? Another example would be where a custom AppInitializer interface is defined and the Application gets injected with a Set<AppInitializer> or List<AppInitializer> and all AppInitializers need to be invoked during onCreate. It is not clear to me how these would apply to the statement above. 🤔

I think the way hilt works with tests isn’t always intuitive. Like one instance of application but different instance of SingletonComponent.

Where or when, or how such SingletonComponents are created by the way, if not tied to Application#onCreate?

A lot of the complexity around testing is due to how Gradle runs instrumentation tests. Gradle will run all tests using a single instance of an Application. This means Application#onCreate() gets called only once no matter how many tests and test cases you’re running. It also means that storing any state in the application class will leak that state across all of your test cases. To avoid this issue, Hilt creates and stores the SingletonComponent using the HiltAndroidRule rather than the Application so that each test case gets its own component instance and is independent from other test cases.

However, as you’ve likely seen, using HiltAndroidRule to create the SingletonComponent causes issues if you try to call entry points from Application#onCreate() because the SingletonComponent has not yet been created (and even if you could create one, it’s not clear which one you would use since each test case has its own?).

For cases where you absolutely need to access an entry point in Application#onCreate() we’ve created EarlyEntryPoint, but as you’ve noticed, the binding is created from a completely different component that has the lifetime of the Application rather than the HiltAndroidRule. In general, you should avoid using EarlyEntryPoint unless you have no other choice because it can lead to the issues you described of two instances of a singleton component.

For more details see https://dagger.dev/hilt/early-entry-point#background