One of the benefits of using dependency injection frameworks like Hilt is that it makes testing your code easier.
Unit tests
Hilt isn't necessary for unit tests, since when testing a class that uses constructor injection, you don't need to use Hilt to instantiate that class. Instead, you can directly call a class constructor by passing in fake or mock dependencies, just as you would if the constructor weren't annotated:
Kotlin
@ActivityScoped class AnalyticsAdapter @Inject constructor( private val service: AnalyticsService ) { ... } class AnalyticsAdapterTest { @Test fun `Happy path`() { // You don't need Hilt to create an instance of AnalyticsAdapter. // You can pass a fake or mock AnalyticsService. val adapter = AnalyticsAdapter(fakeAnalyticsService) assertEquals(...) } }
Java
@ActivityScope public class AnalyticsAdapter { private final AnalyticsService analyticsService; @Inject AnalyticsAdapter(AnalyticsService analyticsService) { this.analyticsService = analyticsService; } } public final class AnalyticsAdapterTest { @Test public void happyPath() { // You don't need Hilt to create an instance of AnalyticsAdapter. // You can pass a fake or mock AnalyticsService. AnalyticsAdapter adapter = new AnalyticsAdapter(fakeAnalyticsService); assertEquals(...); } }
End-to-end tests
For integration tests, Hilt injects dependencies as it would in your production code. Testing with Hilt requires no maintenance because Hilt automatically generates a new set of components for each test.
Adding testing dependencies
To use Hilt in your tests, include the hilt-android-testing
dependency in your
project:
Groovy
dependencies { // For Robolectric tests. testImplementation 'com.google.dagger:hilt-android-testing:2.51.1' // ...with Kotlin. kaptTest 'com.google.dagger:hilt-android-compiler:2.51.1' // ...with Java. testAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.51.1' // For instrumented tests. androidTestImplementation 'com.google.dagger:hilt-android-testing:2.51.1' // ...with Kotlin. kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.51.1' // ...with Java. androidTestAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.51.1' }
Kotlin
dependencies { // For Robolectric tests. testImplementation("com.google.dagger:hilt-android-testing:2.51.1") // ...with Kotlin. kaptTest("com.google.dagger:hilt-android-compiler:2.51.1") // ...with Java. testAnnotationProcessor("com.google.dagger:hilt-android-compiler:2.51.1") // For instrumented tests. androidTestImplementation("com.google.dagger:hilt-android-testing:2.51.1") // ...with Kotlin. kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.51.1") // ...with Java. androidTestAnnotationProcessor("com.google.dagger:hilt-android-compiler:2.51.1") }
UI test setup
You must annotate any UI test that uses Hilt with @HiltAndroidTest
. This
annotation is responsible for generating the Hilt components for each test.
Also, you need to add the HiltAndroidRule
to the test class. It manages the
components' state and is used to perform injection on your test:
Kotlin
@HiltAndroidTest class SettingsActivityTest { @get:Rule var hiltRule = HiltAndroidRule(this) // UI tests here. }
Java
@HiltAndroidTest public final class SettingsActivityTest { @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this); // UI tests here. }
Next, your test needs to know about the Application
class that Hilt
automatically generates for you.
Test application
You must execute instrumented tests that use Hilt in an Application
object
that supports Hilt. The library provides HiltTestApplication
for use in tests.
If your tests need a different base application, see Custom application for
tests.
You must set your test application to run in your instrumented tests or Robolectric tests. The following instructions aren't specific to Hilt, but are general guidelines on how to specify a custom application to run in tests.
Set the test application in instrumented tests
To use the Hilt test application in instrumented tests, you need to configure a new test runner. This makes Hilt work for all of the instrumented tests in your project. Perform the following steps:
- Create a custom class that extends
AndroidJUnitRunner
in theandroidTest
folder. - Override the
newApplication
function and pass in the name of the generated Hilt test application.
Kotlin
// A custom runner to set up the instrumented application class for tests. class CustomTestRunner : AndroidJUnitRunner() { override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } }
Java
// A custom runner to set up the instrumented application class for tests. public final class CustomTestRunner extends AndroidJUnitRunner { @Override public Application newApplication(ClassLoader cl, String className, Context context) throws ClassNotFoundException, IllegalAccessException, InstantiationException { return super.newApplication(cl, HiltTestApplication.class.getName(), context); } }
Next, configure this test runner in your Gradle file as described in the instrumented unit test guide. Make sure you use the full classpath:
Groovy
android { defaultConfig { // Replace com.example.android.dagger with your class path. testInstrumentationRunner "com.example.android.dagger.CustomTestRunner" } }
Kotlin
android { defaultConfig { // Replace com.example.android.dagger with your class path. testInstrumentationRunner = "com.example.android.dagger.CustomTestRunner" } }
Set the test application in Robolectric tests
If you use Robolectric to test your UI layer, you can specify which application
to use in the robolectric.properties
file:
application = dagger.hilt.android.testing.HiltTestApplication
Alternatively, you can configure the application on each test individually by
using Robolectric's @Config
annotation:
Kotlin
@HiltAndroidTest @Config(application = HiltTestApplication::class) class SettingsActivityTest { @get:Rule var hiltRule = HiltAndroidRule(this) // Robolectric tests here. }
Java
@HiltAndroidTest @Config(application = HiltTestApplication.class) class SettingsActivityTest { @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this); // Robolectric tests here. }
If you use an Android Gradle Plugin version lower than 4.2, enable
transforming @AndroidEntryPoint
classes in local unit tests by applying the
following configuration in your module's build.gradle
file:
Groovy
hilt { enableTransformForLocalTests = true }
Kotlin
hilt { enableTransformForLocalTests = true }
More information about enableTransformForLocalTests
in the Hilt
documentation.
Testing features
Once Hilt is ready to use in your tests, you can use several features to customize the testing process.
Inject types in tests
To inject types into a test, use @Inject
for field injection. To tell Hilt to
populate the @Inject
fields, call hiltRule.inject()
.
See the following example of an instrumented test:
Kotlin
@HiltAndroidTest class SettingsActivityTest { @get:Rule var hiltRule = HiltAndroidRule(this) @Inject lateinit var analyticsAdapter: AnalyticsAdapter @Before fun init() { hiltRule.inject() } @Test fun `happy path`() { // Can already use analyticsAdapter here. } }
Java
@HiltAndroidTest public final class SettingsActivityTest { @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this); @Inject AnalyticsAdapter analyticsAdapter; @Before public void init() { hiltRule.inject(); } @Test public void happyPath() { // Can already use analyticsAdapter here. } }
Replace a binding
If you need to inject a fake or mock instance of a dependency, you need to tell Hilt not to use the binding that it used in production code and to use a different one instead. To replace a binding, you need to replace the module that contains the binding with a test module that contains the bindings that you want to use in the test.
For example, suppose your production code declares a binding for
AnalyticsService
as follows:
Kotlin
@Module @InstallIn(SingletonComponent::class) abstract class AnalyticsModule { @Singleton @Binds abstract fun bindAnalyticsService( analyticsServiceImpl: AnalyticsServiceImpl ): AnalyticsService }
Java
@Module @InstallIn(SingletonComponent.class) public abstract class AnalyticsModule { @Singleton @Binds public abstract AnalyticsService bindAnalyticsService( AnalyticsServiceImpl analyticsServiceImpl ); }
To replace the AnalyticsService
binding in tests, create a new Hilt module in
the test
or androidTest
folder with the fake dependency and annotate it
with @TestInstallIn
. All the tests in that folder are injected with the fake
dependency instead.
Kotlin
@Module @TestInstallIn( components = [SingletonComponent::class], replaces = [AnalyticsModule::class] ) abstract class FakeAnalyticsModule { @Singleton @Binds abstract fun bindAnalyticsService( fakeAnalyticsService: FakeAnalyticsService ): AnalyticsService }
Java
@Module @TestInstallIn( components = SingletonComponent.class, replaces = AnalyticsModule.class ) public abstract class FakeAnalyticsModule { @Singleton @Binds public abstract AnalyticsService bindAnalyticsService( FakeAnalyticsService fakeAnalyticsService ); }
Replace a binding in a single test
To replace a binding in a single test instead of all tests, uninstall a Hilt
module from a test using the @UninstallModules
annotation and create a new
test module inside the test.
Following the AnalyticsService
example from the previous version, begin by telling
Hilt to ignore the production module by using the @UninstallModules
annotation
in the test class:
Kotlin
@UninstallModules(AnalyticsModule::class) @HiltAndroidTest class SettingsActivityTest { ... }
Java
@UninstallModules(AnalyticsModule.class) @HiltAndroidTest public final class SettingsActivityTest { ... }
Next, you must replace the binding. Create a new module within the test class that defines the test binding:
Kotlin
@UninstallModules(AnalyticsModule::class) @HiltAndroidTest class SettingsActivityTest { @Module @InstallIn(SingletonComponent::class) abstract class TestModule { @Singleton @Binds abstract fun bindAnalyticsService( fakeAnalyticsService: FakeAnalyticsService ): AnalyticsService } ... }
Java
@UninstallModules(AnalyticsModule.class) @HiltAndroidTest public final class SettingsActivityTest { @Module @InstallIn(SingletonComponent.class) public abstract class TestModule { @Singleton @Binds public abstract AnalyticsService bindAnalyticsService( FakeAnalyticsService fakeAnalyticsService ); } ... }
This only replaces the binding for a single test class. If you want to replace
the binding for all test classes, use the @TestInstallIn
annotation from the
section above. Alternatively, you can put the test binding in the test
module
for Robolectric tests, or in the androidTest
module for instrumented tests.
The recommendation is to use @TestInstallIn
whenever possible.
Binding new values
Use the @BindValue
annotation to easily bind fields in your test into the Hilt
dependency graph. Annotate a field with @BindValue
and it will be bound under
the declared field type with any qualifiers that are present for that field.
In the AnalyticsService
example, you can replace AnalyticsService
with a
fake by using @BindValue
:
Kotlin
@UninstallModules(AnalyticsModule::class) @HiltAndroidTest class SettingsActivityTest { @BindValue @JvmField val analyticsService: AnalyticsService = FakeAnalyticsService() ... }
Java
@UninstallModules(AnalyticsModule.class) @HiltAndroidTest class SettingsActivityTest { @BindValue AnalyticsService analyticsService = FakeAnalyticsService(); ... }
This simplifies both replacing a binding and referencing a binding in your test by allowing you to do both at the same time.
@BindValue
works with qualifiers and other testing annotations. For example,
if you use testing libraries such as
Mockito, you could use it in a
Robolectric test as follows:
Kotlin
... class SettingsActivityTest { ... @BindValue @ExampleQualifier @Mock lateinit var qualifiedVariable: ExampleCustomType // Robolectric tests here }
Java
... class SettingsActivityTest { ... @BindValue @ExampleQualifier @Mock ExampleCustomType qualifiedVariable; // Robolectric tests here }
If you need to add a multibinding,
you can use the @BindValueIntoSet
and @BindValueIntoMap
annotations in place
of @BindValue
. @BindValueIntoMap
requires you to also annotate the field
with a map key annotation.
Special cases
Hilt also provides features to support nonstandard use cases.
Custom application for tests
If you cannot use HiltTestApplication
because your test application needs to
extend another application, annotate a new class or interface with
@CustomTestApplication
, passing in the value of the base class you want the
generated Hilt application to extend.
@CustomTestApplication
will generate an Application
class ready for testing
with Hilt that extends the application you passed as a parameter.
Kotlin
@CustomTestApplication(BaseApplication::class) interface HiltTestApplication
Java
@CustomTestApplication(BaseApplication.class) interface HiltTestApplication { }
In the example, Hilt generates an Application
named
HiltTestApplication_Application
that extends the BaseApplication
class. In
general, the name of the generated application is the name of the annotated
class appended with _Application
. You must set the generated Hilt test
application to run in your instrumented tests or
Robolectric tests as described in Test
application.
Multiple TestRule objects in your instrumented test
If you have other TestRule
objects in your test, there are multiple ways to
ensure that all of the rules work together.
You can wrap the rules together as follows:
Kotlin
@HiltAndroidTest class SettingsActivityTest { @get:Rule var rule = RuleChain.outerRule(HiltAndroidRule(this)). around(SettingsActivityTestRule(...)) // UI tests here. }
Java
@HiltAndroidTest public final class SettingsActivityTest { @Rule public RuleChain rule = RuleChain.outerRule(new HiltAndroidRule(this)) .around(new SettingsActivityTestRule(...)); // UI tests here. }
Alternatively, you can use both rules at the same level as long as the
HiltAndroidRule
executes first. Specify the execution order using the
order
attribute in the @Rule
annotation. This only works in JUnit version
4.13 or higher:
Kotlin
@HiltAndroidTest class SettingsActivityTest { @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) var settingsActivityTestRule = SettingsActivityTestRule(...) // UI tests here. }
Java
@HiltAndroidTest public final class SettingsActivityTest { @Rule(order = 0) public HiltAndroidRule hiltRule = new HiltAndroidRule(this); @Rule(order = 1) public SettingsActivityTestRule settingsActivityTestRule = new SettingsActivityTestRule(...); // UI tests here. }
launchFragmentInContainer
It is not possible to use launchFragmentInContainer
from the
androidx.fragment:fragment-testing
library with Hilt, because it relies on an
activity that is not annotated with @AndroidEntryPoint
.
Use the
launchFragmentInHiltContainer
code from the
architecture-samples
GitHub
repository instead.
Use an entry point before the singleton component is available
The @EarlyEntryPoint
annotation provides an escape hatch when a Hilt entry
point needs to be created before the singleton component is available in a
Hilt test.
More information about @EarlyEntryPoint
in the
Hilt documentation.