添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement . We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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.

I think there is some room for improvement in the documentation, but the suggested approach here is to move your printerFactory into a base class that both your production app and the custom test app will extend.

class BaseApp : Application() {
   @Inject lateinit var printerFactory: PrinterFactory

Your prod app now just extends the new base:

@HiltAndroidApp
class MyApplication: BaseApp()

and your custom test app should too:

@CustomTestApplication(BaseApp::class)
interface HiltTestApplication

you'll also have to updates usages of your app, instead of casting the app context or app to your production app class cast it to the base class.

Unfortunately that results in:

error: [Hilt]
    public static abstract interface HiltTestApplication {
  @CustomTestApplication does not support application classes (or super classes) with @Inject fields. Found BaseApp with @Inject fields [printerFactory].
          

Ah, my bad, I forgot about that check. :(

Added on ecd9e8f, you'll find the reason in the commit message. Due to injection timing we opted for banning injected fields in the test app instead of letting users run into NPEs due to injection not occurring in the App's onCreate() during a test.

There is probably a few paths you can take but if your intent is to scope and make printerFactory available to those who can get a hold of the app context, then maybe just scoping it (adding @Singleton to the provider) and creating an entry point for it might be a nice way make it available in a more on-demand fashion.

class BaseApp : Application() {
    fun getPrinterFactory() =
        EntryPointsAccessors.fromApplication(this, PrinterFactoryEntryPoint::class.java).getPrinterFactory()
    @EntryPoint
    @InstallIn(ApplicationComponent::class)
    interface PrinterFactoryEntryPoint {
        fun getPrinterFactory(): PrinterFactory
          

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)

Tougee, 0xGuybrush, sergiomr88, hgross, sivarooban-swirepay, Maragues, dhruv2295, imohsenb, and akexorcist reacted with thumbs up emoji 0xGuybrush and sivarooban-swirepay reacted with hooray emoji rohitkaradkar reacted with rocket emoji All reactions

@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()
          

Is there any possible way to execute instrumented tests with the following structure using hilt 2.32-alpha?
It looks like there is no straight forward way with the @EntryPoint/@EntryPointAccessors pattern inside the BaseApplication. The structure works fine for the app, but not for instrumented tests with MyCustomTestApplication and MyCustomTestRunner.

I am able to run instrumented tests based on MyCustomTestApplication when I remove the entry point from BaseApplication.

Any clues how to get injection wiht hilt inside BaseApplication working?

Example:

// src
@CustomTestApplication(BaseApplication::class)
interface TestApplication
// src
open class BaseApplication : Application() {
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface SomeDependencyEntryPoint {
        fun getSomeDependency(): ISomeDependency
    // no @Inject, since "@CustomTestApplication does not support application classes (or super classes) with @Inject fields."
    // we make use of the "EntryPointAccessors-pattern" instead
    lateinit var someDependency: ISomeDependency
    override fun onCreate() {
        super.onCreate()
        someDependency = EntryPointAccessors.fromApplication(this, SomeDependencyEntryPoint::class.java).getSomeDependency()
        someDependency.doSomething();
// src
@HiltAndroidApp
open class MainApplication : BaseApplication()
// androidTest src
@CustomTestApplication(BaseApplication::class)
interface MyCustomTestApplication
// A custom runner to set up the instrumented application class for tests.
// see https://developer.android.com/training/dependency-injection/hilt-testing
// must be referenced in the build.gradle
class MyCustomTestRunner : AndroidJUnitRunner() {
    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
        // compilation errors in the IDE here are expected until first build (according to HILT docs)
        return super.newApplication(cl, MyCustomTestApplication_Application::class.java.name, context)
// build.gradle of app module
android {
    defaultConfig {
        testInstrumentationRunner "eu.hgross.example.MyCustomTestApplication"
          

Hi @hgross,

We're currently working on a feature to allow entry points in such a case (with some caveats); however, it's still important to understand why this doesn't work by default in Hilt so that you can decide if this is something you really want to do.

First, if you have to reuse the BaseApplication in Gradle instrumentation tests be careful about mutable state because the same application instance is used for all test classes and test cases in Gradle instrumentation tests making it very easy to leak state across test cases. Instead, try moving all application state into the Hilt SingletonComponent. Each test case (even for Gradle instrumentation tests) will get a new instance of the SingletonComponent, so @Singleton scoped bindings will not be leaked across test cases.

In addition, try to avoid entry point calls in the application. While calling entry points lazily, as in #2033 (comment) sometimes works, it would better to provide it via a module instead, if possible.

That said, we do understand that there are some cases that required calling entry points in Application#onCreate(), e.g. some libraries require some static configuration. The main reason this doesn't work by default in Hilt is that at the time of Application#onCreate() there is no SingletonComponent available (remember that in Hilt tests, the SingletonComponent instance is created per test case rather than per Application). 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.

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. 🤔

@mhernand40, for now we still don't allow using @Inject fields in the application class (it's possible we allow this in the future, but that's questionable).

However, you can create an @EarlyEntryPoint to replace the @Inject fields for the application class, like:

class BaseApplication extends Application {
  // Use EarlyEntryPoint rather than @Inject for these fields since they need to be accessed in onCreate in tests.
  @EarlyEntryPoint
  interface ApplicationEarlyEntryPoint {
    Foo foo();
    Bar bar();
  private Foo foo;
  private Bar bar;
  @Override
  public void onCreate() {
    super.onCreate();
    foo = EarlyEntryPoints.get(this, ApplicationEarlyEntryPoint.class).foo();
    bar = EarlyEntryPoints.get(this, ApplicationEarlyEntryPoint.class).bar();

While you could even just create an @EarlyEntryPoint injector by adding an inject method for the application, like: void inject(MyTestApplication app), it would lead to double injection if you use the base application in non-test applications.

Thanks for the detailed answer and valuable testing hints @bcorso . One of the use cases @mhernand40 mentioned does apply to my requirements. I want to register a HILT-injected component to the lifecycle callbacks in the application class. This component is repsonsible to orchestrate initialization and teardown/shutdown.

Regarding the BaseApplication: This turns out to be a quite time consuming endavour for me as well, since I am dealing with a multi-module project and finding a structure to re-use the custom test-runner for instrumented tests in sub-/parent-modules is not at all straight forward to me. Is there any up-to-date multi-module best-practice project-example for multi-module projects and the whole testing pyramid (unit, instrumented/integration, ui-tests)?

Although @EarlyEntryPoint works for injecting into the Application#onCreate, the same dependency if is injected to another Android component such as a Service seems produce two instances of such dependency (even though annotated with @singleton).

In my case I have something like this:

@Singleton
class Foo { @Inject constructor}

then in my BaseApplication I have this:

private Foo;
@EarlyEntryPoint
@InstallIn(SingletonComponent.class)
interface HiltEntryPoint { 
    Foo getFoo(); 
onCreate(){
    foo = EarlyEntryPoints.get(this, HiltEntryPoint.class).getFoo();

Then in one of my service I have:

@Inject
Foo foo;

The result is that the foo in my Service is different than the foo in my BaseApplication.

@namgk, that's correct. The @Inject Foo field does not come from the EarlyEntryPoint component. If you want the early entry point Foo in your service you would have to also get it with an early entry point EarlyEntryPoints.get(this, HiltEntryPoint.class).getFoo().

However, you may be able to avoid this issue if it's possible for you to create Foo lazily rather than in Application#onCreate and just use a normal entry point, like:

  @EntryPoint
  @InstallIn(SingletonComponent.class)
  interface HiltEntryPoint { 
      Foo getFoo(); 
  private Foo foo() {
    // Create this lazily rather than in onCreate to avoid needing an @EarlyEntryPoint
    return EntryPoints.get(this, HiltEntryPoint.class).getFoo();
Works for me. Interesting things to learn along the way :)
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?