Skip to content

Commit dd64ca2

Browse files
committed
Hilt: allow custom injection support for tests.
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.
1 parent 344e135 commit dd64ca2

File tree

8 files changed

+41
-3
lines changed

8 files changed

+41
-3
lines changed

java/dagger/hilt/EntryPoint.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package dagger.hilt;
1818

1919
import static java.lang.annotation.RetentionPolicy.CLASS;
20+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
2021

2122
import java.lang.annotation.ElementType;
2223
import java.lang.annotation.Retention;
@@ -44,7 +45,7 @@
4445
*
4546
* @see <a href="https://dagger.dev/hilt/entry-points">Entry points</a>
4647
*/
47-
@Retention(CLASS)
48+
@Retention(RUNTIME)
4849
@Target(ElementType.TYPE)
4950
@GeneratesRootInput
5051
public @interface EntryPoint {}

java/dagger/hilt/android/internal/managers/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ android_library(
3333
"SavedStateHandleModule.java",
3434
"ServiceComponentManager.java",
3535
"ViewComponentManager.java",
36+
"TestInjectInterceptor.java",
3637
],
3738
exports = [":saved_state_handle_holder"],
3839
deps = [
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dagger.hilt.android.internal.managers;
2+
3+
/**
4+
* This class does nothing in production or in tests when running under Hilt testing framework. However, the calls
5+
* to TestInjectInterceptor.injectForTesting() are done in a few strategic places just before Hilt does the injection
6+
* into Android Components.
7+
*
8+
* As a result this class enables non-Hilt based frameworks to take over the injection process.
9+
*/
10+
public class TestInjectInterceptor {
11+
/**
12+
* This method always returns false by default. However, if this method is intercepted during testing
13+
* by frameworks like Robolectric, the intercepting code can take over the injection process and
14+
* instruct Hilt to skip doing anything extra for this instance.
15+
*
16+
* Return false if no custom injection was done and Hilt should continue as normal. Return true
17+
* if the testing framework has takes over the injection process and Hilt should skip any extra
18+
* work.
19+
*/
20+
public static boolean injectForTesting(Object injectTo) {
21+
return false;
22+
}
23+
}

java/dagger/hilt/android/processor/internal/androidentrypoint/Generators.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,9 @@ && ancestorExtendsGeneratedHiltClass(metadata)) {
379379
methodSpecBuilder
380380
.beginControlFlow("if (!injected)")
381381
.addStatement("injected = true")
382+
.beginControlFlow("if ($T.injectForTesting(this))", ClassNames.TEST_INJECT_INTERCEPTOR)
383+
.addStatement("return")
384+
.endControlFlow()
382385
.addStatement(
383386
"(($T) $L).$L($L)",
384387
metadata.injectorClassName(),
@@ -395,6 +398,10 @@ && ancestorExtendsGeneratedHiltClass(metadata)) {
395398
.beginControlFlow("if (!injected)")
396399
.beginControlFlow("synchronized (injectedLock)")
397400
.beginControlFlow("if (!injected)")
401+
.beginControlFlow("if ($T.injectForTesting(this))", ClassNames.TEST_INJECT_INTERCEPTOR)
402+
.addStatement("injected = true")
403+
.addStatement("return")
404+
.endControlFlow()
398405
.addStatement(
399406
"(($T) $T.generatedComponent(context)).$L($L)",
400407
metadata.injectorClassName(),
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Keep for the reflective cast done in EntryPoints.
22
# See b/183070411#comment4 for more info.
3-
-keep,allowobfuscation,allowshrinking @dagger.hilt.android.EarlyEntryPoint class *
3+
-keep,allowobfuscation,allowshrinking @dagger.hilt.android.EarlyEntryPoint class *
4+
-assumenosideeffects class dagger.hilt.android.internal.managers.TestInjectInterceptor { *; }

java/dagger/hilt/android/qualifiers/ActivityContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@
2424

2525
/** Annotation for a {@code Context} that corresponds to the activity. */
2626
@Qualifier
27-
@Retention(RetentionPolicy.CLASS)
27+
@Retention(RetentionPolicy.RUNTIME)
2828
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
2929
public @interface ActivityContext {}

java/dagger/hilt/android/qualifiers/ApplicationContext.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package dagger.hilt.android.qualifiers;
1818

1919
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
2022
import java.lang.annotation.Target;
2123
import javax.inject.Qualifier;
2224

2325
/** Annotation for an Application Context dependency. */
2426
@Qualifier
27+
@Retention(RetentionPolicy.RUNTIME)
2528
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
2629
public @interface ApplicationContext {}

java/dagger/hilt/processor/internal/ClassNames.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ public final class ClassNames {
162162
get("dagger.hilt.android.internal.testing", "InternalTestRoot");
163163
public static final ClassName TEST_INJECTOR =
164164
get("dagger.hilt.android.internal.testing", "TestInjector");
165+
166+
public static final ClassName TEST_INJECT_INTERCEPTOR = get("dagger.hilt.android.internal.managers", "TestInjectInterceptor");
165167
public static final ClassName TEST_APPLICATION_COMPONENT_MANAGER =
166168
get("dagger.hilt.android.internal.testing", "TestApplicationComponentManager");
167169
public static final ClassName TEST_APPLICATION_COMPONENT_MANAGER_HOLDER =

0 commit comments

Comments
 (0)