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:
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:
Click the RANDOM NUMBER button and you’ll see a screen like in Figure 19.3:
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:
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:
-
FunNumberFragmentdelegates all the logic to a ViewModel you implemented inFunNumberViewModel. -
FunNumberViewModeldelegates the logic to aFunNumberServiceimplementation. -
FunNumberServiceImplis the implementation ofFunNumberServicethat uses aNumberGeneratorto generate a random number, as well as theFunNumberEndpointimplementation Retrofit provides for the actual request to the server.
RandomFunNumber also contains:
-
SplashActivityas the splash for the app. -
MainActivityas the container forFunNumberFragment.
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:
The diagram contains the @Modules and @Components for RandomFunNumber. In particular:
- You use the predefined
ApplicationComponentandActivityComponent. -
ApplicationComponentcontains bindings from four different@Modules:ApplicationModule,NetworkModule,NetworkingModuleandSchedulersModule. -
ActivityComponentcontains bindings from two different@Modules:ActivityModuleandNavigationModule. - You include
NavigationModuleandNetworkingModulebecause 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:
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:
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:
Select Create test and you’ll end up with the dialog in Figure 19.9:
Now, select JUnit 4 for the testing library and click OK to get the screen in Figure 19.10:
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:
- As mentioned, threading is very important in general, especially when you work with
LiveDataorRx. Here, you initializeInstantTaskExecutorRuleto useScheduler’s instant implementation. This allows you to run all the operations sequentially. - This is the property for the instance of the object to test. In this case, it’s
FunNumberViewModel. -
FunNumberViewModeldepends onFunNumberService. Here, you define the property that will contain the instance of the fakeFunNumberService. Its code is in the testShared source folder. - In
@Before, you initializeFunNumberViewModel, passing the fakeNumberServiceimplementation as a constructor parameter. This is the advantage of using constructor injection. You don’t need Dagger here. - Here, you invoke
refreshNumber()on theFunNumberViewModelinstance you’re testing. This uses theFunNumberServiceimplementation you passed as a parameter. -
getOrAwaitValue()is a utility extension function forLiveDatathat allows you to wait for the result. The source code is in LiveDataTestUtil.kt in the source folder for the test build type. - Finally, you check that your result is what you expected.
Now, run the test, selecting the´Run ‘FunNumberViewModelTest’ option shown in Figure 19.11:
The test runs successfully, as in Figure 19.12:
Aside from this specific test’s functionality, there are three important things to note:
- 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.
- You didn’t use Dagger or Hilt at all.
- 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:
- Use Robolectric.
- 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:
MainActivityFunNumberFragment
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:
- Add the dependency to use Hilt testing with Robolectric. This is a definition for the test build type.
- Use
kaptTestto 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:
- Contains the
@AndroidEntryPointannotation that tags theActivityas a Hilt entry point. - Defines a
navigatorproperty you initialize using@Inject. - Uses
navigatorto displayFunNumberFragment.
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:
-
You annotate the test with
@HiltAndroidTest. This enables the Hilt annotation processor to generate the code to create the dependency graph for the test. -
In Main.kt inside app, you used
@HiltAndroidAppto tell Hilt whichApplicationimplementation to use. Now, you need to do the same thing for the tests. Hilt providesHiltTestApplicationas theApplicationimplementation for this purpose. Here, you use the Robolectric@Configannotation to do this, but you could get the same effect by editing robolectric.properties. You’ll see how that works soon. -
Due to bugs in the current library, you need to explicitly define
RobolectricTestRunneras theTestRunnerto 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. -
You create an instance of the
HiltAndroidRuleJUnit rule inhiltAndroidRule. This allows you to create and destroy the Hilt-provided dependency graph at each test execution. -
You invoke
inject()onhiltAndroidRuleat the beginning of each test. As you’ll see later, this injects objects from the Hilt test dependency graph into the test itself. -
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:
- Configure the
ActivityScenarioAPI to launchMainActivity. - Replace the existing
Navigatorimplementation with a fake one. - 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:
- Initialize a new JUnit Rule of type
ActivityScenarioRule<MainActivity>in theactivityScenarioRuleproperty . - Use the
orderattribute for the@get:Ruleannotation. You need this when you have more than one rule in the same file and you want to give them a specific execution order.HiltAndroidRuleneeds to be the first rule to run — settingorder = 0allows you to ensure that it is. - Access the
scenarioproperty on theactivityScenarioRuleto launch theActivityyou set as parameter type value inActivityScenarioRule<MainActivity>. In this case, it’sMainActivity.
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:
- 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 forNavigationModule, adding one for theFunNumberServiceimplementation. You don’t actually needFunNumberServicein this test, but you need to provide one toNavigator. - With
@BindValue, you’re binding an instance ofFakeNavigatorto theNavigatortype. With this and the previous definitions, you’ve basically replaced theNavigatorimplementation in the NavigationModule module with the mock implementation. -
FakeNavigatoris a simpleNavigatorimplementation that stores the reference to theDestinationyou use. The test consists of checking that you’ve used aDestinationand that it’s aFragmentDestination.
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:
- Use
HiltAndroidRuleto ask Hilt to generate a dependency graph to use when a test executes. - Uninstall the bindings you defined in one or more
@Modules using@UninstallModules. - 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:
- Add the testing Hilt dependencies for the instrumented test.
- Create an
@AndroidEntryPointActivityto use as the container for theFragmentunder test. - Implement a utility class to launch the
Fragmentunder test into the Hilt-enabledActivity. - Create a custom
AndroidJUnitRunnerthat usesHiltTestApplicationinstead of the one Android provides by default, then configure it in build.gradle. - 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:
- Add the dependency that lets you use Hilt testing in instrumentation tests. That’s why the definition is for the androidTest build type.
- Use
kaptAndroidTestto 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:
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:
- Annotate the test class with
@HiltAndroidTest. - Ask Dagger, through Hilt, to uninstall the bindings in
ActivityModule.Bindings. These are the ones relating toFunNumberService. - Replace the
FunNumberServiceimplementation you just uninstalled withFakeFunNumberService. - Invoke
inject()on thehiltAndroidRuleto trigger the injection into the test. - Launch
FunNumberFragmentinHiltActivityForTestusinglaunchFragmentInHiltContainer(). - Use Espresso to check that the
Fragmentdisplays what it gets fromFunNumberService.
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:
-
Uninstall more than one
@Moduleusing a comma-separated list of@Module’s classes. -
Use
@Injectto get the reference to some objects in the testing dependency graph. In this case, you get the reference toTestScheduler, which you need for RxJava. -
Use
@BindValueto replace some of the objects in the testing dependency graph. -
Replace the entire
SchedulersModulewith a newSchedulersModuleyou define in the same FunNumberServiceImplHiltTest file. In this case, you’re replacing theSchedulers the app uses with the ones you need for the tests. In particular, you replacedmainThreadwithSchedulers.trampoline()andio()withTestScheduler.
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
@UninstallModulesand replace some of them using@BindValue. - You can replace all the bindings for a
@Moduleby uninstalling it with@UninstallModulesand 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.