Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

19. Testing With Hilt
Written by Massimo Carli

In Chapter 17, “Hilt — Dagger Made Easy”, you learned that one of Hilt’s main goals is to make testing easier. In the first chapter, you also learned that simplifying testing is also one of the main reasons to use dependency injection.

Using constructor injection, you can create instances of classes to test, then pass mocks or fakes directly to them as their primary constructor parameters. So where does Hilt come in? How does it make tests easier to implement and run?

To understand this, think about what Dagger and Hilt actually are. Dagger gives you a declarative way to define the dependency graph for a given app. Using @Modules and @Components, you define which objects to create, what their lifecycles are and how they depend upon one other.

Hilt makes things easier with a predefined set of @Components and @Scopes. In particular, using @HiltAndroidApp, you define the main @EntryPoint for the app bound to the Application lifecycle. You do the same with ActivityComponent, ServiceComponent and others you met in the previous chapters. In general, you define a dependency graph containing the instances of classes you inject.

Here’s the point. The objects you use when you run a test are, in most cases, not the same objects you use when running the app. The dependency graph isn’t the same.

Hilt gives you an easy way to decide which objects are in the dependency graph for the app and which objects are there for the tests.

In this chapter, you’ll learn how Hilt helps you implement tests for your app. In particular, you’ll see how to use Hilt to run:

  • Robolectric UI tests
  • Instrumentation tests

Hilt only supports Robolectric tests and instrumentation tests because with constructor injection, it’s easy to implement unit tests without Dagger. You’ll see how shortly.

To learn how to implement tests with Hilt, you’ll work on the RandomFunNumber app. This simple app lets you push a button to get a random number and some interesting facts about that number. It’s also perfect for learning about testing with Hilt.

Note: As you know, Hilt is still in alpha version and so are its testing libraries. During this chapter, you’ll implement some configuration caveats that need to be there to make the tests run successfully. Some of these solve bugs in the library that might be resolved by the time you read this chapter.

The RandomFunNumber app

In this chapter, you’ll implement tests for RandomFunNumber. To start, use Android Studio to open the RandomFunNumber project from the starter folder in this chapter’s materials. When you open the project, you’ll see the structure in Figure 19.1:

Figure 19.1 — Initial Project Structure
Figure 19.1 — Initial Project Structure

As you can see, this app uses some of the modules you already used in the previous chapters.

Build and run to see the screen in Figure 19.2:

Figure 19.2 — Initial Empty Screen
Figure 19.2 — Initial Empty Screen

Click the RANDOM NUMBER button and you’ll see a screen like in Figure 19.3:

Figure 19.3 — A possible result
Figure 19.3 — A possible result

The output in your case is probably different because, when you press the button, you generate a random value and send a request to numbersapi.com to get some useful information about it. Every time you click the button, you’ll get a different number and a different description.

This is a very simple app that contains everything you need to learn how to write tests using the tools and API Hilt provides. Before doing that, however, you’ll learn about RandomFunNumber’s:

  • Architecture
  • Hilt configuration

After you understand how the app works, you’ll start writing tests.

RandomFunNumber’s architecture

The class diagram in Figure 19.4 gives you a high-level description of RandomFunNumber’s main components:

Figure 19.4 — RandomFunNumber class diagram
Figure 19.4 — RandomFunNumber class diagram

In this diagram, you see the components FunNumberFragment uses to implement the feature that gets the random value and fetches the text from the numbersapi.com service.

As you see:

  1. FunNumberFragment delegates all the logic to a ViewModel you implemented in FunNumberViewModel.
  2. FunNumberViewModel delegates the logic to a FunNumberService implementation.
  3. FunNumberServiceImpl is the implementation of FunNumberService that uses a NumberGenerator to generate a random number, as well as the FunNumberEndpoint implementation Retrofit provides for the actual request to the server.

RandomFunNumber also contains:

  • SplashActivity as the splash for the app.
  • MainActivity as the container for FunNumberFragment.

Both use a Navigation implementation you find in libs.ui.navigation, which you already used in the app for the previous chapters.

Of course, RandomFunNumber uses Hilt. Next, you’ll look at its configuration.

The Hilt configuration

RandomFunNumber only contains a few components. It has a relatively simple Hilt configuration, which you can see in the diagram in Figure 19.5:

Figure 19.5 — RandomFunNumber dependency diagram
Figure 19.5 — RandomFunNumber dependency diagram

The diagram contains the @Modules and @Components for RandomFunNumber. In particular:

  1. You use the predefined ApplicationComponent and ActivityComponent.
  2. ApplicationComponent contains bindings from four different @Modules: ApplicationModule, NetworkModule, NetworkingModule and SchedulersModule.
  3. ActivityComponent contains bindings from two different @Modules: ActivityModule and NavigationModule.
  4. You include NavigationModule and NetworkingModule because their definitions are in different modules.

The code in the starter project in the materials for this chapter contains quite a few @Modules. One reason is modularization. For instance, NetworkingModule and NavigationModule are in the libs.networking and libs.ui.navigation modules.

Another reason is testing. As you’ll see later, you’ll replace threading during tests. That’s why the Scheduler’s bindings are in SchedulersModule and not directly in ApplicationModule.

Now, it’s time to implement the tests for RandomFunNumber.

Implementing RandomFunNumber’s tests

You have the background you need to use RandomFunNumber to practice implementing tests with the utilities Hilt provides. In the following paragraphs, you’ll learn how to:

  • Structure your project for testing.
  • Leverage constructor injection to implement a unit test.
  • Use Robolectric and Hilt for UI testing.
  • Implement UI instrumentation tests with Hilt and Espresso.

Keep in mind that most of the configurations are already in the starter project in the material for this chapter.

Defining the project structure for testing

Use Android Studio to open the starter project from the materials for this chapter. Next, select Project View and look at the build types for the app module in Figure 19.6:

Figure 19.6 — App module build types
Figure 19.6 — App module build types

Highlighted in Figure 19.6, you have:

  • androidTest for instrumentation tests.
  • main for the main app code.
  • test for unit and Robolectric tests.
  • testShared for some code in common between test and androidTest.

You’ll see each of these build types in detail in the following paragraphs. For the moment, just open testShared and look at its content:

Figure 19.7 — Classes shared between tests
Figure 19.7 — Classes shared between tests

It contains some fakes and stubs you’ll use in your tests.

Note: If you want to learn more about fakes, mocks and stubs, Android Test-Driven Development by Tutorials is the right place for you.

Implementing unit tests with constructor injection

As you learned in the previous chapters of this book, constructor injection makes tests easier to write because you simply create an instance of the object to test and pass some fakes or stubs to it as parameters of its primary constructor.

With this kind of testing, there’s not much benefit to using Dagger. To see this for yourself, you’ll create a unit test for FunNumberViewModel.

Open FunNumberViewModel.kt in ui.displaynumber and click the class name, then press Control-Enter. This gives you the following pop-up:

Figure 19.8 — Create a test for FunNumberViewModel
Figure 19.8 — Create a test for FunNumberViewModel

Select Create test and you’ll end up with the dialog in Figure 19.9:

Figure 19.9 — Select the JUnit 4 option
Figure 19.9 — Select the JUnit 4 option

Now, select JUnit 4 for the testing library and click OK to get the screen in Figure 19.10:

Figure 19.10 — Select the test building type
Figure 19.10 — Select the test building type

Select the test directory, as in Figure 19.10, and click OK. Android Studio will create a file with the following code:

class FunNumberViewModelTest {
}

Now, open the new FunNumberViewModelTest.kt and add the following code:

class FunNumberViewModelTest {

  @Rule
  @JvmField
  val instantExecutorRule = InstantTaskExecutorRule() // 1

  private lateinit var objectUnderTest: FunNumberViewModel // 2
  private lateinit var funNumberService: FakeFunNumberService // 3

  @Before
  fun setUp() {
    funNumberService = FakeFunNumberService()
    objectUnderTest = FunNumberViewModel(funNumberService) // 4
  }

  @Test
  fun `when refreshNumber invoked you observe FunNumber`() {
    val expectedFunNumber = FunNumber(
        88,
        "Testing Text",
        true,
        "default"
    )
    funNumberService.resultToReturn = expectedFunNumber
    objectUnderTest.refreshNumber() // 5
    val result = objectUnderTest.numberFunFacts.getOrAwaitValue() // 6
    assertEquals(expectedFunNumber, result) // 7
  }
}

Here, you’re testing that, when you invoke refreshNumber() on FunNumberViewModel, you get the expected result. Here are some important things to note:

  1. As mentioned, threading is very important in general, especially when you work with LiveData or Rx. Here, you initialize InstantTaskExecutorRule to use Scheduler’s instant implementation. This allows you to run all the operations sequentially.
  2. This is the property for the instance of the object to test. In this case, it’s FunNumberViewModel.
  3. FunNumberViewModel depends on FunNumberService. Here, you define the property that will contain the instance of the fake FunNumberService. Its code is in the testShared source folder.
  4. In @Before, you initialize FunNumberViewModel, passing the fake NumberService implementation as a constructor parameter. This is the advantage of using constructor injection. You don’t need Dagger here.
  5. Here, you invoke refreshNumber() on the FunNumberViewModel instance you’re testing. This uses the FunNumberService implementation you passed as a parameter.
  6. getOrAwaitValue() is a utility extension function for LiveData that allows you to wait for the result. The source code is in LiveDataTestUtil.kt in the source folder for the test build type.
  7. Finally, you check that your result is what you expected.

Now, run the test, selecting the´Run ‘FunNumberViewModelTest’ option shown in Figure 19.11:

Figure 19.11 — Run FunNumberViewModelTest
Figure 19.11 — Run FunNumberViewModelTest

The test runs successfully, as in Figure 19.12:

Figure 19.12 — FunNumberViewModelTest success
Figure 19.12 — FunNumberViewModelTest success

Aside from this specific test’s functionality, there are three important things to note:

  1. Constructor injection allows you to create the instance of the object under test by passing the reference to fakes or mocks as their primary constructor parameter.
  2. You didn’t use Dagger or Hilt at all.
  3. There are no Android classes involved.

This is an example of the power of dependency injection — and, in particular, of constructor injection.

As mentioned above, FunNumberViewModel doesn’t have any Android-specific dependencies. But how can you write tests that do involve Android-specific components, like Activitys and Fragments?

You have two options:

  1. Use Robolectric.
  2. Create an instrumentation test.

You’re still working in the test build type, so it’s time for Robolectric. :]

Using Robolectric & Hilt for UI tests

Robolectric (http://robolectric.org/) is a testing framework that lets you implement and run tests that depend on the Android environment without an actual implementation of the Android platform. This allows you to run UI tests on the JVM without creating instances of the Android emulator. In this way, tests run quickly and require fewer resources.

For your next step, you’ll use Robolectric and Hilt to run tests for:

  • MainActivity
  • FunNumberFragment

Before the actual test implementation, however, you need to do some setup.

Installing the Hilt testing library for Robolectric

For your first step, you need to add the dependencies for Robolectric’s Hilt testing library. Open build.gradle from app and add the following definition:

// ...
dependencies {
  // ...
  // Hilt for Robolectric tests.
  testImplementation "com.google.dagger:hilt-android-testing:$hilt_version" // 1
  kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version" // 2
}

Here, you:

  1. Add the dependency to use Hilt testing with Robolectric. This is a definition for the test build type.
  2. Use kaptTest to install the annotation processor responsible for generating the testing code from the Hilt definition.

The version is the same as the main Hilt library’s dependency has. Also, note how testing with Hilt requires you to install an annotation processor because it will have to generate some code.

Creating a MainActivity test with Robolectric & Hilt

Your first test will cover MainActivity. Open MainActivity.kt in the ui package of the app module and you’ll see:

@AndroidEntryPoint // 1
class MainActivity : AppCompatActivity() {

  @Inject
  lateinit var navigator: Navigator // 2

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    if (savedInstanceState == null) {
      navigator.navigateTo( // 3
        FragmentDestination(FunNumberFragment(), 
        R.id.anchor_point)
      )
    }
  }
}

This is a very simple Activity that:

  1. Contains the @AndroidEntryPoint annotation that tags the Activity as a Hilt entry point.
  2. Defines a navigator property you initialize using @Inject.
  3. Uses navigator to display FunNumberFragment.

Next, you want to create a test that verifies that the navigator displays FunNumberFragment properly. Start by using the process shown in Figures 19.8-10, create a new RoboMainActivityTest.kt in the test build type.

Initially, your code looks like this:

class RoboMainActivityTest

Before you write the actual test, you need some initial configuration. Change the previous code to this:

@HiltAndroidTest // 1
@Config(application = HiltTestApplication::class) // 2
@RunWith(RobolectricTestRunner::class) // 3
class RoboMainActivityTest {

  @get:Rule
  var hiltAndroidRule = HiltAndroidRule(this) // 4

  @Before
  fun setUp() {
    hiltAndroidRule.inject() // 5
  }

  @Test
  fun whenMainActivityLaunchedNavigatorIsInvokedForFragment() { // 6
    assertTrue(true)
  }
}

This code contains a very important configuration you’ll use in the following tests, as well. Here’s what’s going on:

  1. You annotate the test with @HiltAndroidTest. This enables the Hilt annotation processor to generate the code to create the dependency graph for the test.

  2. In Main.kt inside app, you used @HiltAndroidApp to tell Hilt which Application implementation to use. Now, you need to do the same thing for the tests. Hilt provides HiltTestApplication as the Application implementation for this purpose. Here, you use the Robolectric @Config annotation to do this, but you could get the same effect by editing robolectric.properties. You’ll see how that works soon.

  3. Due to bugs in the current library, you need to explicitly define RobolectricTestRunner as the TestRunner to use for running the tests in this file. You do this using @RunWith. If this bug has been fixed since the time of writing, you should remove this definition.

  4. You create an instance of the HiltAndroidRule JUnit rule in hiltAndroidRule. This allows you to create and destroy the Hilt-provided dependency graph at each test execution.

  5. You invoke inject() on hiltAndroidRule at the beginning of each test. As you’ll see later, this injects objects from the Hilt test dependency graph into the test itself.

  6. Define the function for the test in this file. At the moment, you’re asserting something that you know is true, just to have something to run.

Before running this test, you need to open robolectric.properties from resources in the test build type. Its initial content is:

sdk=28

Robolectric requires this to avoid an annoying error on the API Level of the Android environment you’re testing against. To avoid using what’s at the previous point 2, add the following line:

sdk=28
application=dagger.hilt.android.testing.HiltTestApplication # HERE

Now, you can use Android Studio and run a successful test. Your next step is to implement the test.

Testing MainActivity with Robolectric & Hilt

In the previous section, you created an empty test to verify the Hilt configuration for Robolectric. Now, it’s time to create the actual test.

To do this, you need to:

  1. Configure the ActivityScenario API to launch MainActivity.
  2. Replace the existing Navigator implementation with a fake one.
  3. Write the actual test.

Configuring ActivityScenario

ActivityScenario is part of an API Google provides for testing Activitys. To use this, you need to add the following code to RoboMainActivityTest.kt, which you created earlier:

@HiltAndroidTest
@Config(application = HiltTestApplication::class)
@RunWith(RobolectricTestRunner::class)
class RoboMainActivityTest {

  @get:Rule(order = 0) // 2
  var hiltAndroidRule = HiltAndroidRule(this)

  @get:Rule(order = 1) // 2
  var activityScenarioRule: ActivityScenarioRule<MainActivity> = 
      ActivityScenarioRule(MainActivity::class.java) // 1
  // ...
    @Test
  fun whenMainActivityLaunchedNavigatorIsInvokedForFragment() {
    activityScenarioRule.scenario // 3
  }
}

To use ActivityScenario, you need to:

  1. Initialize a new JUnit Rule of type ActivityScenarioRule<MainActivity> in the activityScenarioRule property .
  2. Use the order attribute for the @get:Rule annotation. You need this when you have more than one rule in the same file and you want to give them a specific execution order. HiltAndroidRule needs to be the first rule to run — setting order = 0 allows you to ensure that it is.
  3. Access the scenario property on the activityScenarioRule to launch the Activity you set as parameter type value in ActivityScenarioRule<MainActivity> . In this case, it’s MainActivity.

Unfortunately, if you now run the test with Android Studio as you did before, you’ll get the following error:

kotlin.UninitializedPropertyAccessException: lateinit property navigator has not been initialized

Don’t worry, this isn’t your fault. :] This is a bug that, at the moment, prevents you from running this test from Android Studio.

Instead, just open a terminal and run the following command:

./gradlew testDebugUnitTest --tests "*.RoboMainActivityTest.*"

The test will successfully run.

It’s an empty test, though. How can you test that MainActivity actually works? Look at its code and you see that MainActivity needs a Navigator.

Replacing the real Navigator with a fake

Now, you’ll replace the actual Navigator implementation with a fake one. To achieve this, add the following code:

@HiltAndroidTest
@Config(application = HiltTestApplication::class)
@RunWith(RobolectricTestRunner::class)
@UninstallModules(ActivityModule::class) // 1
class RoboMainActivityTest {
  // ...
  @BindValue // 2
  @JvmField
  val navigator: Navigator = FakeNavigator() // 3

  @Test
  fun whenMainActivityLaunchedNavigatorIsInvokedForFragment() {
    activityScenarioRule.scenario
    val fakeNav = navigator as FakeNavigator
    assertNotNull(fakeNav.invokedWithDestination)
    assertTrue(fakeNav.invokedWithDestination is FragmentDestination<*>) // 4
  }
}

This code contains everything you need to know about Hilt and testing. As you can see:

  1. By using @UninstallModules(ActivityModule::class), you’re telling Hilt, and then Dagger, to literally uninstall all the bindings you defined in ActivityModule.kt. That file includes bindings for NavigationModule, adding one for the FunNumberService implementation. You don’t actually need FunNumberService in this test, but you need to provide one to Navigator.
  2. With @BindValue, you’re binding an instance of FakeNavigator to the Navigator type. With this and the previous definitions, you’ve basically replaced the Navigator implementation in the NavigationModule module with the mock implementation.
  3. FakeNavigator is a simple Navigator implementation that stores the reference to the Destination you use. The test consists of checking that you’ve used a Destination and that it’s a FragmentDestination.

Now, run the test from the command line and check that it’s successful.

Reviewing your achievements

Great! You implemented your first test using Hilt and Robolectric. You also learned that you can:

  1. Use HiltAndroidRule to ask Hilt to generate a dependency graph to use when a test executes.
  2. Uninstall the bindings you defined in one or more @Modules using @UninstallModules.
  3. Replace one binding at a time using @BindValue.

Keep these points in mind because you’ll use them many times in the following tests.

Implementing instrumented tests with Hilt & Espresso

In the previous section, you used Robolectric and Hilt to implement a UI test for MainActivity. Now, you’ll try another option for testing with Hilt — running an instrumentation test with Espresso.

Note: If you want to learn all about Espresso, Android Test-Driven Development by Tutorials is, again, a great resource.

Note: In the final project in the materials for this chapter, you’ll also find an instrumentation test for MainActivity. Implementing it is a useful exercise.

In this section, you’ll create a more challenging test. This time, you’ll test FunNumberFragment. Creating a test like this isn’t obvious and it requires some preparation.

Usually, before you test a Fragment, you first launch an Activity as its container. With Hilt, the problem is that if the Fragment is an @AndroidEntryPoint, the same must be true for the container Activity. If you just use ActivityScenario, this doesn’t happen automatically. You need to:

  1. Add the testing Hilt dependencies for the instrumented test.
  2. Create an @AndroidEntryPoint Activity to use as the container for the Fragment under test.
  3. Implement a utility class to launch the Fragment under test into the Hilt-enabled Activity.
  4. Create a custom AndroidJUnitRunner that uses HiltTestApplication instead of the one Android provides by default, then configure it in build.gradle.
  5. Implement and run the instrumentation test.

It’s important to do these tasks in sequence.

Note: Don’t worry if FragmentTestutil.kt in the androidTest build type doesn’t compile at the moment. You’ll fix it very soon.

Adding Hilt testing dependencies

To use the Hilt testing library in the instrumentation tests, you need to add the following definition to build.gradle in app:

// ...
dependencies {
  // Hilt for instrumented tests.
  androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version" // 1
  kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version" // 2
  // ...
}

In this case, you need to:

  1. Add the dependency that lets you use Hilt testing in instrumentation tests. That’s why the definition is for the androidTest build type.
  2. Use kaptAndroidTest to install the annotation processor responsible for generating the testing code from the Hilt definition in instrumented tests.

Now, you’re ready to use Hilt testing libraries in the instrumentation tests for RandomFunNumber.

Creating an Activity for testing

First, you need to create an empty Activity that uses @AndroidEntryPoint. Start by creating a folder for the debug build type at the same level as the existing ones. Create a java folder in it and add a package named com.raywenderlich.android.randomfunnumber. In that package, create a new file named HiltActivityForTest.kt and add the following code:

@AndroidEntryPoint // HERE
class HiltActivityForTest : AppCompatActivity()

Important here is the use of @AndroidEntryPoint, which Hilt requires for Activitys containing @AndroidEntryPoint Fragments . This is an Activity you need to launch from the instrumentation test.

To do this, create a file named AndroidManifest.xml in debug and add the following content:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.raywenderlich.android.randomfunnumber">

  <application>
    <activity
      android:name=".HiltActivityForTest"
      android:exported="false" />
  </application>
</manifest>

You’ll end up with the file structure in Figure 19.13:

Figure 19.13 — Debug build type structure
Figure 19.13 — Debug build type structure

Now, FragmentTestutil.kt in androidTest will successfully compile.

Implementing a utility to launch the Fragment

As mentioned earlier, you need a way to launch a Fragment using the HiltActivityForTest you implemented as its container.

Note: Google already provides different versions of this utility in some Codelabs. They’ll probably add it to a future release of the Hilt testing library.

Open FragmentTestUtil.kt in util in the androidTest build type and find the extension function with the following signature:

inline fun <reified T : Fragment> launchFragmentInHiltContainer(
    fragmentArgs: Bundle? = null,
    @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme,
    crossinline action: Fragment.() -> Unit = {}
) {
  // ...
}

The only thing you need to know is that you can now use launchFragmentInHiltContainer() to launch a Fragment that’s an @AndroidEntryPoint for Hilt.

Implementing a custom AndroidJUnitRunner

As you learned when testing with Robolectric, you need to specify the TestRunner for your tests. To do this, you just need to create a custom TestRunner implementation.

Start by creating a new file named HiltTestRunner.kt in a new runner package in the androidTest build type and add the following code:

class HiltTestRunner : AndroidJUnitRunner() {

  override fun newApplication(
      cl: ClassLoader?,
      className: String?,
      context: Context?
  ): Application {
    return super.newApplication(
        cl,
        HiltTestApplication::class.java.name, // HERE
        context)
  }
}

Here, you override newApplication() by passing HiltTestApplication::class.java.name as the value for Application parameter.

Now, you need to set this as the default TestRunner implementation for instrumentation tests.

Open build.gradle and apply the following changes:

// ...
android {
  // ...
  defaultConfig {
    applicationId "com.raywenderlich.android.randomfunnumber"
    minSdkVersion 28
    targetSdkVersion 30
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "com.raywenderlich.android.randomfunnumber.runner.HiltTestRunner" // HERE
  }
  // ...
}
// ...

Here, you replaced the existing value for testInstrumentationRunner with HiltTestRunner’s full name. Now, it’s finally time to write the instrumentation test for FunNumberFragment.

Implementing FunNumberFragment’s test

You now have everything you need to implement the test for FunNumberFragment. Following the process in Figure 19.8-10, create FunNumberFragmentTest.kt in the androidTest build type and add the following code:

@HiltAndroidTest
@UninstallModules(ActivityModule.Bindings::class) // 1
class FunNumberFragmentTest {

  @get:Rule
  var hiltAndroidRule = HiltAndroidRule(this) // 2

  @BindValue
  @JvmField
  val funNumberService: FunNumberService = FakeFunNumberService() // 3

  @Before
  fun setUp() {
    hiltAndroidRule.inject() // 4
  }

  @Test
  fun whenButtonPushedSomeResultDisplayed() {
    (funNumberService as FakeFunNumberService).resultToReturn =
        FunNumber(123, "Funny Number", true, "testValue")
    launchFragmentInHiltContainer<FunNumberFragment>() // 5
    onView(withId(R.id.refresh_fab_button)).perform(click()) // 6
    onView(withId(R.id.fun_number_output)).check(matches(withText("123")))
    onView(withId(R.id.fun_fact_output)).check(matches(withText("Funny Number")))
  }
}

Other than TestRunner’s configuration, this test isn’t much different from the one you implemented using Robolectric. Here, you:

  1. Annotate the test class with @HiltAndroidTest.
  2. Ask Dagger, through Hilt, to uninstall the bindings in ActivityModule.Bindings. These are the ones relating to FunNumberService.
  3. Replace the FunNumberService implementation you just uninstalled with FakeFunNumberService.
  4. Invoke inject() on the hiltAndroidRule to trigger the injection into the test.
  5. Launch FunNumberFragment in HiltActivityForTest using launchFragmentInHiltContainer().
  6. Use Espresso to check that the Fragment displays what it gets from FunNumberService.

Now, use Android Studio to run the test and check that everything works as expected.

Great! You learned how to implement the configuration you need to use Hilt with an instrumented test. This example is similar to the one you implemented with Robolectric.

This is actually almost everything you need to know about Hilt and testing. There’s just one more thing to cover, but you’ll need a more complex example for it.

Replacing an entire @Module

In the previous example, you only replaced some of the bindings you installed as part of a @Module. For instance, in RoboMainActivityTest, you uninstalled ActivityModule, but you added a binding for Navigator.

In some cases, however, you need to replace an entire @Module with another. To see an example of this, create a new file named FunNumberServiceImplHiltTest.kt in the business package in the androidTest build type and add the following code:

@HiltAndroidTest
@UninstallModules( // 1
    SchedulersModule::class,
    NetworkModule::class,
    ApplicationModule::class)
class FunNumberServiceImplHiltTest {

  @Inject
  lateinit var objectUnderTest: FunNumberServiceImpl

  @Inject
  @IOScheduler
  lateinit var testScheduler: Scheduler // 2

  @BindValue
  @JvmField
  val funNumberEndPoint: FunNumberEndpoint = StubFunNumberEndpoint() // 3

  @BindValue
  @JvmField
  val randomGenerator: NumberGenerator = FakeNumberGenerator().apply { // 3
    nextNumber = 123
  }

  @get:Rule
  var hiltAndroidRule = HiltAndroidRule(this)

  @Before
  fun setUp() {
    hiltAndroidRule.inject()
  }

  @Test
  fun whenRandomFunNumberIsInvokedAResultIsReturned() {
    val fakeCallback = FakeCallback<FunNumber>()
    objectUnderTest.randomFunNumber(fakeCallback)
    (testScheduler as TestScheduler).advanceTimeBy(100, TimeUnit.MILLISECONDS)
    val received = fakeCallback.callbackParameter
    Assert.assertNotNull(received)
    if (received != null) {
      with(received) {
        assertEquals(number, 123)
        assertTrue(found)
        assertEquals(text, "Number is: 123")
        assertEquals(type, "validType")
      }
    } else {
      Assert.fail("Something wrong!")
    }
  }

  @Module
  @InstallIn(ApplicationComponent::class) // 4
  object SchedulersModule {

    @Provides
    @ApplicationScoped
    @MainScheduler
    fun provideMainScheduler(): Scheduler = Schedulers.trampoline()

    @Provides
    @ApplicationScoped
    @IOScheduler
    fun provideIoScheduler(): Scheduler = TestScheduler()
  }
}

This is the Hilt version of the test for FunNumberServiceImpl. You can find this class, as a unit test, in the test build type in the final project in the materials for this chapter.

The pattern is the same as you’ve seen in the previous examples. In this case, you:

  1. Uninstall more than one @Module using a comma-separated list of @Module’s classes.

  2. Use @Inject to get the reference to some objects in the testing dependency graph. In this case, you get the reference to TestScheduler, which you need for RxJava.

  3. Use @BindValue to replace some of the objects in the testing dependency graph.

  4. Replace the entire SchedulersModule with a new SchedulersModule you define in the same FunNumberServiceImplHiltTest file. In this case, you’re replacing the Schedulers the app uses with the ones you need for the tests. In particular, you replaced mainThread with Schedulers.trampoline() and io() with TestScheduler.

In this example, you learned that you can replace the binding of an entire @Module by simply uninstalling the initial one and reinstalling a new one. You can define the testing @Module in the same file as the test or as an external file, depending on where you use the new @Module.

Key points

  • Hilt provides a testing library for Robolectric and instrumented tests.
  • You don’t need Dagger to implement unit tests if you use constructor injection.
  • Hilt allows you to replace parts of the app’s dependency graph for testing purposes.
  • Using @HiltAndroidTest, you ask Hilt to generate a dependency graph to use during the execution of a test.
  • You can remove bindings from the dependency graph using @UninstallModules and replace some of them using @BindValue.
  • You can replace all the bindings for a @Module by uninstalling it with @UninstallModules and installing a new @Module.

Great job! In this chapter, you saw how to modify the dependency graph of your app to make Robolectric and instrumentation tests easier to implement and run. You’ve now learned everything you need to know about Hilt.

This is the last chapter of this book that covers Dagger and Hilt. In the very final chapter, you’ll see how you can implement dependency injection in the Busso Server back-end app.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.