-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
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
Hilt: allow custom injection support for tests. #4173
base: master
Are you sure you want to change the base?
Conversation
One of the key decisions with Hilt is bytecode rewriting. It helps simplify the developer experience, but makes things more complicated for testing. As a result Hilt provides additional testing framework that helps mitigate these concerns and allows for great flexibility when it comes to mocking and replacing dependencies for testing. Still, hilt has non-trivial compilation costs. And as the codebase growth, we've observed that the cost for test complication growth even more so than for production code. As a result there is an exploration to avoid using hilt for simpler cases where the value of DI graph in tests is very small, but the additional costs to compile are great. This diff introduces a few small touches to Hilt codegen to allow for a runtime test DI (like a simpler version of Guice) to overtake the injection. Specifically, this diff introduces `TestInjectInterceptor` class with a single empty static method `injectForTesting()`. The codegen for Activities, Fragments, Views, Services, and Broadcasts is adjusted to have the next code: ``` protected void inject() { if (!injected) { injected = true; if (TestInjectInterceptor.injectForTesting(this)) { return; } // rest of Hilt injection code. } ``` For production or tests running under Hilt the additional code does nothing. And for production this code should be eliminated by R8. But for cases where testing framework is able to intercept a call to `TestInjectInterceptor.injectForTesting()` (like Robolectric shadow), the injection can be overtake in a consistent manner for all types of supported android entry points.
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
b1bfa44
to
dd64ca2
Compare
Hey, sorry for the delay here. Have you seen https://dagger.dev/hilt/optional-inject? I think this gives you the tools you need to use a different injector at runtime. So you could use a Hilt application in prod, but otherwise use a different injector in tests. |
@Chang-Eric thanks for pointing out Test-Specific DITo have the same context, let's assume that the codebase is fully onboarded into Hilt. Let's also assume that Robolectric is used for unit tests, specifically Robolectric shadows are used to intercept/override behavior of Android and Hilt classes. The expected usage of such Test-Specific DI will look something like this:
Where Test-DI requirementsFor
Comparing
|
Also, if there is a concern about having |
Hey, sorry the delay here, and thanks for the detailed explanation. To be fairly upfront, I think it is unlikely we'll accept this change. The scope of Hilt is to enable injection through Dagger and not necessarily through other means like via runtime injection. The goal with If you were to do this today in a non-Hilt codebase, you'd need to have each Activity/Fragment have a method that injects itself and in that injection method you could then access your |
Yes, I am aware about the Hilt testing philosophy and existing approach to testing. And unfortunately it doesn't scale well in a large codebase and has significant impact on compile time. The suggestion to do this on per-component (Activity/Fragment/Service/Receiver) would probably work, but it forces production code to use I understand the hesitance of allowing the test hook, but my hope was that if we hide this behind the flag it would be reasonable. This is not an expansive change, and it touches only a few places in Hilt and has no material impact on Hilt operation, principles and unlikely to prevent future changes in the codebase. |
I'm sorry, I don't think I really have a satisfying answer for you. I think our current testing approach doesn't have to have the scaling problems you mention, though we have sort of not elaborated too much on that because our experience is that many Gradle users don't try to limit dependencies too much. But Hilt is designed such that if you are able to manage what your test depends on, you can easily cut out different Unfortunately though, while this test hook is simple in code right now, it just represents too large of a conceptual scope increase to the library. That scope increase just generally poses long-term risks to the maintainability of the project. |
One of the key decisions with Hilt is bytecode rewriting. It helps simplify the developer experience, but makes things more complicated for testing. As a result Hilt provides additional testing framework that helps mitigate these concerns and allows for great flexibility when it comes to mocking and replacing dependencies for testing.
Still, hilt has non-trivial compilation costs. And as the codebase growth, we've observed that the cost for test complication growth even more so than for production code. As a result there is an exploration to avoid using hilt for simpler cases where the value of DI graph in tests is very small, but the additional costs to compile are great.
This diff introduces a few small touches to Hilt codegen to allow for a runtime test DI (like a simpler version of Guice) to overtake the injection.
Specifically, this diff introduces
TestInjectInterceptor
class with a single empty static methodinjectForTesting()
. The codegen for Activities, Fragments, Views, Services, and Broadcasts is adjusted to have the next code:For production or tests running under Hilt the additional code does nothing. And for production this code should be eliminated by R8. But for cases where testing framework is able to intercept a call to
TestInjectInterceptor.injectForTesting()
(like Robolectric shadow), the injection can be overtaken in a consistent manner for all types of supported android entry points.Additional changes to @entrypoint, @ApplicationContext and @ActivityContext are done to allow runtime retention so that runtime-based testing DI can rely on annotations when resolving dependencies dynamically.