Introduction
Hilt makes testing easier by bringing the power of dependency injection to your Android tests. Hilt allows your tests to easily access Dagger bindings, provide new bindings, or even replace bindings. Each test gets its own set of Hilt components so that you can easily customize bindings at a per-test level.
Many of the testing APIs and functionality described in this documentation are based upon an unstated philosophy of what makes a good test. For more details on Hilt’s testing philosophy see here.
Test Setup
Note: For Gradle users, make sure to first add the Hilt test build dependencies as described in the Gradle setup guide.
To use Hilt in a test:
- Annotate the test with
@HiltAndroidTest
, - Add the
HiltAndroidRule
test rule, - Use
HiltTestApplication
for your Android Application class.
For example:
@HiltAndroidTest
public class FooTest {
@Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
...
}
@HiltAndroidTest
class FooTest {
@get:Rule val hiltRule = HiltAndroidRule(this)
...
}
Note that setting the application class for a test (step 3 above) is dependent on whether the test is a Robolectric or instrumentation test. For a more detailed guide on how to set the test application for a particular test environment, see Robolectric testing or Instrumentation testing. The remainder of this doc applies to both Robolectric and instrumentation tests.
If your test requires a custom application class, see the section on custom test application.
If your test requires multiple test rules, see the section on Hilt rule order to determine the proper placement of the Hilt rule.
Accessing bindings
A test often needs to request bindings from its Hilt components. This section describes how to request bindings from each of the different components.
Accessing SingletonComponent bindings
An SingletonComponent
binding can be injected directly into a test using an
@Inject
annotated field. Injection doesn’t occur until calling
HiltAndroidRule#inject()
.
@HiltAndroidTest
class FooTest {
@Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
@Inject Foo foo;
@Test
public void testFoo() {
assertNull(foo);
hiltRule.inject();
assertNotNull(foo);
}
}
@HiltAndroidTest
class FooTest {
@get:Rule val hiltRule = HiltAndroidRule(this)
@Inject lateinit var foo: Foo
@Test
fun testFoo() {
hiltRule.inject()
assertNotNull(foo)
}
}
Accessing ActivityComponent bindings
Requesting an ActivityComponent
binding requires an instance of a Hilt
Activity
. One way to do this is to define a nested activity within your test
that contains an @Inject
field for the binding you need. Then create an
instance of your test activity to get the binding.
@HiltAndroidTest
class FooTest {
@AndroidEntryPoint
public static final class TestActivity extends AppCompatActivity {
@Inject Foo foo;
}
// Create the activity through standard testing APIs and get an
// instance as testActivity. Make sure the activity has gone through
// onCreate()
...
// Now just access the foo which has been injected on the activity directly
Foo foo = testActivity.foo;
}
@HiltAndroidTest
class FooTest {
@AndroidEntryPoint
class TestActivity : AppCompatActivity() {
@Inject lateinit var foo: Foo
}
// Create the activity through standard testing APIs and get an
// instance as testActivity. Make sure the activity has gone through
// onCreate()
...
// Now just access the foo which has been injected on the activity directly
val foo = testActivity.foo
}
Alternatively, if you already have a Hilt activity instance available in your
test, you can get any ActivityComponent
binding using an
EntryPoint
.
@HiltAndroidTest
class FooTest {
@EntryPoint
@InstallIn(ActivityComponent.class)
interface FooEntryPoint {
Foo getFoo();
}
...
Foo foo = EntryPoints.get(activity, FooEntryPoint.class).getFoo();
}
@HiltAndroidTest
class FooTest {
@EntryPoint
@InstallIn(ActivityComponent::class)
interface FooEntryPoint {
fun getFoo() : Foo
}
...
val foo = EntryPoints.get(activity, FooEntryPoint::class.java).getFoo()
}
Accessing FragmentComponent bindings
A FragmentComponent
binding can be accessed in a similar way to an
ActivityComponent
binding. The main
difference is that accessing a FragmentComponent
binding requires both an
instance of a Hilt Activity
and a Hilt Fragment
.
@HiltAndroidTest
class FooTest {
@AndroidEntryPoint
public static final class TestFragment extends Fragment {
@Inject Foo foo;
}
...
Foo foo = testFragment.foo;
}
@HiltAndroidTest
class FooTest {
@AndroidEntryPoint
class TestFragment : Fragment() {
@Inject lateinit var foo: Foo
}
...
val foo = testFragment.foo
}
Alternatively, if you already have a Hilt fragment instance available in your
test, you can get any FragmentComponent
binding using an
EntryPoint
.
@HiltAndroidTest
class FooTest {
@EntryPoint
@InstallIn(FragmentComponent.class)
interface FooEntryPoint {
Foo getFoo();
}
...
Foo foo = EntryPoints.get(fragment, FooEntryPoint.class).getFoo();
}
@HiltAndroidTest
class FooTest {
@EntryPoint
@InstallIn(FragmentComponent::class)
interface FooEntryPoint {
fun getFoo() : Foo
}
...
val foo = EntryPoints.get(fragment, FooEntryPoint::class.java).getFoo()
}
Warning:Hilt does not currently support FragmentScenario
because there is no way to specify an activity class, and Hilt requires a Hilt
fragment to be contained in a Hilt activity. One workaround for this is to
launch a Hilt activity and then attach your fragment.
Replacing bindings
It’s often useful for tests to be able to replace a production binding with a fake or mock binding to make tests more hermetic or easier to control in test. The next sections describe some ways to accomplish this in Hilt.
@TestInstallIn
A Dagger module annotated with @TestInstallIn
allows users to replace an
existing @InstallIn
module for all tests in a given source set. For example,
suppose we want to replace ProdDataServiceModule
with FakeDataServiceModule
.
We can accomplish this by annotating FakeDataServiceModule
with
@TestInstallIn
, as shown below:
@Module
@TestInstallIn(
components = SingletonComponent.class,
replaces = ProdDataServiceModule.class)
interface FakeDataServiceModule {
@Binds DataService bind(FakeDataService impl);
}
@Module
@TestInstallIn(
components = SingletonComponent::class,
replaces = ProdDataServiceModule::class)
interface FakeDataServiceModule {
@Binds fun bind(impl: FakeDataService): DataService
}
A @TestInstallIn
module can be included in the same source set as your test
sources, as shown below:
:foo
|_ srcs/test/java/my/project/foo
|_ FooTest.java
|_ BarTest.java
|_ FakeDataServiceModule.java
However, if a particular @TestInstallIn
module is needed in multiple Gradle
modules, we recommend putting it in its own Gradle module (usually the same
one as the fake), as shown below:
:dataservice-testing
|_ srcs/main/java/my/project/dataservice/testing
|_ FakeDataService.java
|_ FakeDataServiceModule.java
// This depends on `testImplementation project(":dataservice-testing")`
:foo/build.gradle
// This depends on `testImplementation project(":dataservice-testing")`
:bar/build.gradle
Putting the @TestInstallIn
in the same Gradle module as the fake has a number
of benefits. First, it ensures that all clients that depend on the fake properly
replace the production module with the test module. It also avoids duplicating
FakeDataServiceModule
for every Gradle module that needs it.
Note that @TestInstallIn
applies to all tests in a given source set. For cases
where an individual test needs to replace a binding that is specific to the
given test, the test can either be moved into its own source set, or it can use
Hilt testing features such as @UninstallModules
,
@BindValue
, and nested @InstallIn
modules
to replace bindings specific to that test. These features will be described in
more detail in the following sections.
@UninstallModules
Warning:Test classes that use @UninstallModules
, @BindValue
, or nested
@InstallIn
modules result in a custom component being generated for that test.
While this may be fine in most cases, it does have an impact on build speed. The
recommended approach is to use @TestInstallIn
modules instead.
A test annotated with @UninstallModules
can uninstall production
@InstallIn
modules for that particular test (unlike @TestInstallIn
, it has
no effect on other tests). Once a module is uninstalled, the test can install
new, test-specific bindings for that particular test.
@UninstallModules(ProdFooModule.class)
@HiltAndroidTest
public class FooTest {
// ... Install a new binding for Foo
}
@UninstallModules(ProdFooModule::class)
@HiltAndroidTest
class FooTest {
// ... Install a new binding for Foo
}
There are two ways to install a new binding for a particular test:
- Add an
@InstallIn
module nested within the test that provides the binding. - Add an
@BindValue
field within the test that provides the binding.
These two approaches are described in more detail in the next sections.
Note: @UninstallModules
can only uninstall @InstallIn
modules, not
@TestInstallIn
modules. If a @TestInstallIn
module needs to be uninstalled
the module must be split into two separate modules: a @TestInstallIn
module
that replaces the production module with no bindings (i.e. only removes the
production module), and a @InstallIn
module that provides the standard fake
so that @UninstallModules
can uninstall the provided fake.
Nested @InstallIn modules
Warning:Test classes that use @UninstallModules
, @BindValue
, or nested
@InstallIn
modules result in a custom component being generated for that test.
While this may be fine in most cases, it does have an impact on build speed. The
recommended approach is to use @TestInstallIn
modules instead.
Normally, @InstallIn
modules are installed in the Hilt components of every
test. However, if a binding needs to be installed only in a particular test,
that can be accomplished by nesting the @InstallIn
module within the test
class.
@HiltAndroidTest
public class FooTest {
// Nested modules are only installed in the Hilt components of the outer test.
@Module
@InstallIn(SingletonComponent.class)
static class FakeBarModule {
@Provides
static Bar provideBar(...) {
return new FakeBar(...);
}
}
...
}
@HiltAndroidTest
class FooTest {
// Nested modules are only installed in the Hilt components of the outer test.
@Module
@InstallIn(SingletonComponent::class)
object FakeBarModule {
@Provides fun provideBar() = Bar()
}
...
}
Thus, if there is another test that needs to provision the same binding with a different implementation, it can do that without a duplicate binding conflict.
In addition to static nested @InstallIn
modules, Hilt also supports inner
(non-static) @InstallIn
modules within tests. Using an inner module allows the
@Provides
methods to reference members of the test instance.
Note: Hilt does not support @InstallIn
modules with constructor parameters.
@BindValue
Warning:Test classes that use @UninstallModules
, @BindValue
, or nested
@InstallIn
modules result in a custom component being generated for that test.
While this may be fine in most cases, it does have an impact on build speed. The
recommended approach is to use @TestInstallIn
modules instead.
For simple bindings, especially those that need to also be accessed in the test methods, Hilt provides a convenience annotation to avoid the boilerplate of creating a module and method normally required to provision a binding.
@BindValue
is an annotation that allows you to easily bind fields in your
test into the Dagger graph. To use it, just annotate a field with @BindValue
and it will be bound to the declared field type with any qualifiers that are
present on the field.
@HiltAndroidTest
public class FooTest {
...
@BindValue Bar fakeBar = new FakeBar();
}
@HiltAndroidTest
class FooTest {
...
@BindValue
@JvmField
val fakeBar: Bar = FakeBar()
}
Note that @BindValue
does not support the use of scope annotations since the
binding’s scope is tied to the field and controlled by the test. The field’s
value is queried whenever it is requested, so it can be mutated as necessary for
your test. If you want the binding to be effectively singleton, just ensure that
the field is only set once per test case, e.g. by setting the field’s value from
either the field’s initializer or from within an @Before
method of the test.
Similarly, Hilt also has a convenience annotation for multibindings with
@BindValueIntoSet
, @BindElementsIntoSet
, and @BindValueIntoMap
to
support @IntoSet
, @ElementsIntoSet
, and @IntoMap
respectively. (Note
that @BindValueIntoMap
requires the field to also be annotated with a map key
annotation.)
Warning:Be careful when using @BindValue
or
non-static inner modules with ActivityScenarioRule
.
ActivityScenarioRule
creates the activity before calling the @Before
method,
so if an @BindValue
field is initialized in @Before
(or later), then it’s
possible for the Activity to inject the binding in its unitialized state. To
avoid this, try initializing the @BindValue
field in the field’s initializer.
Custom test application
Every Hilt test must use a Hilt test application as the Android application
class. Hilt comes with a default test application, HiltTestApplication
,
which extends MultiDexApplication
;
however, there are cases where a test may need to use a different base class.
@CustomTestApplication
If your test requires a custom base class, @CustomTestApplication
can
be used to generate a Hilt test application that extends the given base class.
To use @CustomTestApplication
, just annotate a class or interface with
@CustomTestApplication
and specify the base class in the annotation value:
// Generates MyCustom_Application.class
@CustomTestApplication(MyBaseApplication.class)
interface MyCustom {}
// Generates MyCustom_Application.class
@CustomTestApplication(MyBaseApplication::class)
interface MyCustom
In the above example, Hilt will generate an application named
MyCustom_Application
that extends MyBaseApplication
. In general, the name of
the generated application will be the name of the annotated class appended with
_Application
. If the annotated class is a nested class, the name will also
include the name of the outer class separated by an underscore. Note that the
class that is annotated is irrelevant, other than for the name of the generated
application.
Best practices
As a best practice, avoid using @CustomTestApplication
and instead use
HiltTestApplication
in your tests. In general, having your Activity, Fragment,
etc. be independent of the parent they are contained in makes it easier to
compose and reuse it in the future.
However, if you must use a custom base application, there are some subtle differences with the production lifecycle to be aware of.
One difference is that instrumentation tests use the same application instance for every test and test case. Thus, it’s easy to accidentally leak state across test cases when using a custom test application. Instead, it’s better to avoid storing any test or test case dependendent state in your application.
Another difference is that the Hilt component in a test application is not
created in super#onCreate
. This restriction is mainly due to fact that some of
Hilt’s features (e.g. @BindValue
) rely on the test instance,
which is not available in tests until after Application#onCreate
is called.
Thus, unlike production applications, custom base applications must avoid
calling into the component during Application#onCreate
. This includes
injecting members into the application. To prevent this issue, Hilt doesn’t
allow injection in the base application.
Hilt rule order
If your test uses multiple test rules, make sure that the HiltAndroidRule
runs
before any other test rules that require access to the Hilt component. For
example ActivityScenarioRule
calls Activity#onCreate
, which (for Hilt activities) requires the Hilt
component to perform injection. Thus, the ActivityScenarioRule
should run
after the HiltAndroidRule
to ensure that the component has been properly
initialized.
Note: If you’re using JUnit < 4.13 use RuleChain
to specify the order instead.
@HiltAndroidTest
public class FooTest {
// Ensures that the Hilt component is initialized before running the ActivityScenarioRule
@Rule(order = 0) public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
@Rule(order = 1)
public ActivityScenarioRule scenarioRule =
new ActivityScenarioRule(MyActivity.class);
}
@HiltAndroidTest
class FooTest {
// Ensures that the Hilt component is initialized before running the ActivityScenarioRule
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val scenarioRule = ActivityScenarioRule(MyActivity::class.java)
}