[go: nahoru, domu]

Testing

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:

  1. Annotate the test with @HiltAndroidTest,
  2. Add the HiltAndroidRule test rule,
  3. Use HiltTestApplication for your Android Application class.

For example:

Java
Kotlin
@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().

Java
Kotlin
@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.

Java
Kotlin
@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.

Java
Kotlin
@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.

Java
Kotlin
@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.

Java
Kotlin
@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:

Java
Kotlin
@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.

Java
Kotlin
@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.

Java
Kotlin
@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.

Java
Kotlin
@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:

Java
Kotlin
// 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.

Java
Kotlin
@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)
}