Article
Dagger Hilt: Testing injected Android components with code coverage
November 17, 2020
Dagger Hilt is Google’s new opinionated add-on for setting up Dagger 2 for Android. Through its tie-ins with androidx
and preconfigured components, it should be able to meet the common dependency injection demands of most apps.
Google also lays out a testing philosophy that Hilt can help realize, using the real Dagger graph in tests. The goal of this article is to make that philosophy a reality, by providing all of the build script pieces and an inheritance trick needed to set up testing an Android project with code coverage using Hilt, Robolectric, Espresso and JaCoCo.
- 1: Configure JaCoCo
- 2: Setup Robolectric and Espresso
- 3a: Configure Hilt with the Gradle plugin
- 3b: Configure Hilt without the Gradle plugin
This article won’t explain how to write tests for Robolectric and Espresso, or how to use Hilt’s testing utilities, so refer to the documentation for those projects for examples.
Step 1: Configure JaCoCo
The necessary JaCoCo configuration looks basically the same as a normal Android JaCoCo setup. Since it’s not a trivial task to set up, here is a sample build configuration that creates separate test reports for each application variant while also allowing for exclusions:
Step 2: Setup Robolectric and Espresso
The next step is setting up Robolectric and Espresso. Together, these allow running UI-like tests in the JVM without needing an emulator.
Step 3a: Configure Hilt with the Gradle plugin
There are two options for setting up Hilt, with or without the accompanied Gradle Plugin. For completeness, this is the normal setup required for Hilt at runtime, without tests. In the project’s root build.gradle.kts
, we’ll add the Gradle plugin:
And in the app/build.gradle.kts
, we will apply the plugin and add the runtime dependencies:
Because the Hilt Gradle plugin performs a bytecode transformation, running Robolectric tests now will cause JaCoCo to complain that the execution data for any transformed class annotated with @AndroidEntryPoint
doesn’t match. To get around this, we can “lift” the transformation to a subclass that contains no functional code that we care about, which allows the actual implementation to remain untouched and trackable by JaCoCo:
Another note: Currently, the hilt.enableTransformForLocalTests
only works with Gradle, and does not work if running the tests through Android Studio directly (tracked by https://github.com/google/dagger/pull/1811 and https://issuetracker.google.com/37003772 . To workaround this, we can avoid using the Grade Plugin entirely (see below).
Step 3b: Configure Hilt without the Gradle plugin
In lieu of using the Hilt Gradle plugin, the desired superclass for an entry point can be passed to the @AndroidEntryPoint
, and then the entry point directly extends from the generated Hilt_*
class. Using the generated code directly means the root build.gradle.kts
can remain untouched, and the app/build.gradle.kts
doesn’t apply the plugin:
Although we don’t need to workaround the bytecode transformation for JaCoCo when the Hilt Gradle plugin isn’t applied, there is still an issue with having to pass the superclass to @AndroidEntryPoint
. If the superclass has generic type arguments, those can’t be specified in the annotation. The workaround for this is a similar “lift” for the transformation, to ensure that the direct superclass has no type arguments:
And that’s it! With Hilt, Robolectric, Espresso, and JaCoCo, we can test actual Android objects that are injected with Dagger with code coverage. This allows for an incredibly useful set of tests between pure unit tests and full UI tests, that ensure that an application’s actual Dagger graph and all of its resolved dependencies are performing as expected. Using tools like @UninstallModules
, modules for the edges of an application (such as networking) can be replaced, allowing for fast, non-flaky tests that exercise a lot of code that may previously have gone untested.
Current Caveats:
There are a few shortcomings and limitations currently that might be fixed in the future. In no particular order, here are a few:
- Robolectric tests currently can’t be written JUnit 5 directly, blocked by https://github.com/robolectric/robolectric/issues/3477 . Robolectric tests can be run with JUnit 5 Vintage Engine, so pure Kotlin tests can use JUnit 5 while Robolectric tests are still using JUnit 4.
- As mentioned above, the
hilt.enableTransformForLocalTests
only works when running tests through Gradle, and does not work if running the tests through Android Studio directly: https://github.com/google/dagger/issues/1956 and https://issuetracker.google.com/issues/37076369 . - Hilt does not currently work with
FragmentScenario
directly, since the fragment under test must be contained in a Hilt activity. As a workaround, a simple@AndroidEntryPoint TestActivity : AppCompatActivity()
can be defined (also added to thedebug/AndroidManifest.xml
) xml for use in tests. Also see https://developer.android.com/training/dependency-injection/hilt-testing#launchfragment for an equivalentlaunchFragmentInHiltContainer
definition.
-
Alex Vanyo
Software Engineer