Closed RobertBaruch closed 2 years ago
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 https://github.com/google/dagger/commit/ecd9e8f07ce0f1cd04ccc23c0cd4efd5e5a47461, 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
}
}
Yes, that works. I ended up putting this code outside the application, but otherwise it works fine. I think this is major enough to deserve going in the documentation, because it's kind of a tripping hazard 😄
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
@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 https://github.com/google/dagger/issues/2033#issuecomment-671578781 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)?
Thanks for the suggestion @bcorso! Replacing @Inject
fields with the EarlyEntryPoint
seems to be working on my end. 🙂
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();
}
Thanks Brad,
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?
On Thu, May 6, 2021 at 8:37 PM Brad Corso @.***> wrote:
@namgk https://github.com/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 using 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(); }
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/google/dagger/issues/2033#issuecomment-834039252, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHU7IEBZ5332ZHJ7Y5P7QTTMNN7FANCNFSM4PZJJ64Q .
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
Thanks Brad for the detailed response, I totally missed the HiltAndroidRule, makes sense now.
On Fri, May 7, 2021 at 8:15 AM Brad Corso @.***> wrote:
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
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/google/dagger/issues/2033#issuecomment-834507265, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHU7IDBAXYWP7LBDBDMU63TMP7ZZANCNFSM4PZJJ64Q .
This is a pretty common issue from my experience, sometimes it works, sometimes not.
What I normally do, is to uninstall everything from the device, maybe do a clean and rebuild/reinstall. I think this has to do with the way Android does partial apk updates in development.
On Wed, Jul 7, 2021 at 6:59 AM Dawid Hyży @.***> wrote:
I am getting java.lang.RuntimeException: Unable to create application com.example.HiltTestApplication_Application: java.lang.IllegalStateException: The component was not created. Check that you have added the HiltAndroidRule. in the BaseApplication. Did you encounter that? My test class has HiltAndroidRule
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/google/dagger/issues/2033#issuecomment-875628346, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHU7IFSEVJEJB4XXALACZ3TWRMT3ANCNFSM4PZJJ64Q .
I've been trying to follow the steps above, but I'm getting the following error, when trying to access context from the application's onCreate.
MainApplication
@HiltAndroidApp
open class MainApplication : BaseApplication() {
HiltTestApplication
@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication
BaseApplication
abstract class BaseApplication: Application()
lateinit var foo: Foo
@EarlyEntryPoint
@InstallIn(SingletonComponent::class)
internal interface ApplicationEarlyEntryPoint {
val foo: Foo
}
override fun onCreate() {
super.onCreate()
foo = EarlyEntryPoints.get(this <---- ERROR HERE, ApplicationEarlyEntryPoint::class.java).foo
......
Results in:
Expected application context to implement GeneratedComponentManagerHolder. Check that you're passing in an application context that uses Hilt.
Is there a step I have missed somewhere? Thanks in advance!
Can you check what your application class is when you're running? (Just throw in a break point or a print in there before you call EarlyEntryPoints
)
Thanks @Chang-Eric for the suggestion. That helped me to spot that there was some misconfiguration in the CustomTestRunner. I updated that and applied the suggestion above to use the MyApplication_Application
and all is working.
guys I am still getting following error
java.lang.IllegalStateException: Hilt test, com.chargeatfriends.android.ui.settings.login.SignInFragmentTest, cannot use a @HiltAndroidApp application but found com.chargeatfriends.android.Application. To fix, configure the test to use HiltTestApplication or a custom Hilt test application generated with @CustomTestApplication.
at dagger.hilt.internal.Preconditions.checkState(Preconditions.java:83)
at dagger.hilt.android.internal.testing.MarkThatRulesRanRule.<init>(MarkThatRulesRanRule.java:63)
at dagger.hilt.android.testing.HiltAndroidRule.<init>(HiltAndroidRule.java:36)
at com.chargeatfriends.android.ui.settings.login.SignInFragmentTest.<init>(SignInFragmentTest.kt:36)
@kyodgorbek you will need to try the suggestions in the error message. If you have specific questions/issues please give more information.
@bcorso I am doing as suggested but test fails
@kyodgorbek, you're going to have to give more information for us to help you.
Also, since this issue is already closed, it's probably better to start a new issue with all of these details.
I have an application that needs injection:
Other code in the app uses
application.printerFactory
.The
printerFactory
is provided by this:Now, for instrumented tests, I need to override this with a test version:
And so I get this error:
Well, I can't use HiltTestApplication because I need to use MyApplication, which has dependencies injected.
So I added this:
And so I get this error:
If
MyApplication
cannot be annotated with@HiltAndroidApp
, then how is it expected to inject things?Instructions unclear, ended up in inconsistent state.