diff --git a/docs/docs/recipes/27-hilt.md b/docs/docs/recipes/27-hilt.md new file mode 100644 index 00000000..b2b2b921 --- /dev/null +++ b/docs/docs/recipes/27-hilt.md @@ -0,0 +1,150 @@ +--- +keywords: [Hilt, Dagger, dependency injection, hiltViewModel, AndroidEntryPoint, HiltAndroidTest, HiltAndroidRule, HiltTestApplication, GeneratedComponent, ComposableTestActivity] +--- + +import OpenNew from '@site/static/img/open_new.svg'; + +# Testing composables that use Hilt + +If a composable under test gets its dependencies from [Hilt ](https://developer.android.com/training/dependency-injection/hilt-android), for example through `hiltViewModel()`, the test fails with an error like this: + +``` +java.lang.IllegalStateException: Given component holder class dev.testify.ComposableTestActivity does not implement interface dagger.hilt.internal.GeneratedComponent or interface dagger.hilt.internal.GeneratedComponentManager +``` + +Hilt can only inject into activities annotated with `@AndroidEntryPoint`. Testify's `ComposableTestActivity`, which hosts the composable, doesn't have that annotation. The fix is to host the composable in your own `@AndroidEntryPoint` subclass of `ComposableTestActivity`, and launch it with `ComposableScreenshotScenarioRule`. + +:::note + +`ComposableScreenshotRule` always launches `ComposableTestActivity`, so it can't be used with Hilt. Use `ComposableScreenshotScenarioRule`, which lets you choose the activity. + +::: + +## 1. Add the Hilt testing dependencies + +In the module's build file, add Hilt's testing library, and run the Hilt compiler on your `androidTest` sources. Use the same Hilt version as your app: + +```groovy +dependencies { + androidTestImplementation "com.google.dagger:hilt-android-testing:" + kspAndroidTest "com.google.dagger:hilt-compiler:" + androidTestImplementation "androidx.test:core-ktx:" +} +``` + +`core-ktx` provides the `launchActivity` function used below. + +## 2. Use a Hilt test runner + +Hilt tests must run with `HiltTestApplication`. Create a test runner in your `androidTest` sources: + +```kotlin +class HiltTestRunner : AndroidJUnitRunner() { + override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} +``` + +Set it as your instrumentation runner. The Testify Gradle Plugin reads this setting, so `screenshotTest` and `screenshotRecord` use the new runner too. + +```groovy +android { + defaultConfig { + testInstrumentationRunner "com.example.HiltTestRunner" + } +} +``` + +## 3. Create a host activity + +In your `debug` source set, `src/debug/java`, create a subclass of `ComposableTestActivity` annotated with `@AndroidEntryPoint`: + +```kotlin +@AndroidEntryPoint +class HiltComposableTestActivity : ComposableTestActivity() +``` + +Code in the `debug` source set can't see your `androidTest` dependencies, so add Testify to its compile classpath: + +```groovy +dependencies { + debugCompileOnly "dev.testify:testify-compose:" + debugCompileOnly "dev.testify:testify:" +} +``` + +Both are needed, because `ComposableTestActivity` implements an interface from the core `testify` library. `compileOnly` is enough: when the tests run, the Testify classes come from the test APK, just as they do for `ComposableTestActivity`. Using `debugImplementation` instead adds Testify's own test dependencies to your app, and they can conflict with the versions your `androidTest` sources use. + +Declare the activity in the manifest of the same source set, `src/debug/AndroidManifest.xml`, alongside `ComposableTestActivity`: + +```xml + + + + + + +``` + +## 4. Write the test + +Annotate the test class with `@HiltAndroidTest`, and add `HiltAndroidRule` so it runs before the Testify rule: + +```kotlin +@HiltAndroidTest +class HomeScreenScreenshotTest { + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val rule = ComposableScreenshotScenarioRule() + + @ScreenshotInstrumentation + @Test + fun default() { + launchActivity().use { scenario -> + rule + .withScenario(scenario) + .setCompose { + // HomeScreen gets its view model with hiltViewModel() + HomeScreen() + } + .assertSame() + } + } +} +``` + +`ComposableScreenshotScenarioRule` renders the composable into the host activity, so `hiltViewModel()` and other Hilt lookups resolve through `HiltComposableTestActivity`. + +If your test launches one of your app's own `@AndroidEntryPoint` activities instead of a Compose host, you don't need a custom host activity. Use the test runner and `HiltAndroidRule` from steps 2 and 4 with `ScreenshotScenarioRule`. + +## A worked example + +The `FlixHilt` sample module follows these steps exactly, and runs on CI: + +- [HiltComposableTestActivity.kt ](https://github.com/ndtp/android-testify/blob/main/Samples/Flix/FlixHilt/src/debug/java/dev/testify/samples/flix/hilt/HiltComposableTestActivity.kt) and [src/debug/AndroidManifest.xml ](https://github.com/ndtp/android-testify/blob/main/Samples/Flix/FlixHilt/src/debug/AndroidManifest.xml) — step 3 +- [HiltTestRunner.kt ](https://github.com/ndtp/android-testify/blob/main/Samples/Flix/FlixHilt/src/androidTest/java/dev/testify/samples/flix/hilt/HiltTestRunner.kt) — step 2 +- [HiltComposableScreenshotTest.kt ](https://github.com/ndtp/android-testify/blob/main/Samples/Flix/FlixHilt/src/androidTest/java/dev/testify/samples/flix/hilt/HiltComposableScreenshotTest.kt) — step 4 + +It is a module of its own, and that is the one decision worth copying. + +:::caution + +`testInstrumentationRunner` is set per module, so pointing an existing module at a Hilt runner +replaces its `Application` for **every** test in that module, Hilt or not. + +Check what your own `Application` does before you do this. In the `Flix` sample it supplies Coil's +`ImageLoader` through `ImageLoaderFactory`; replacing it left the image-backed screenshot tests +capturing before their images had drawn, which is why the Hilt example is a separate module. If your +module has tests that depend on your `Application`, either give them what they need directly in +`@Before` — see [Fixing blank or incomplete screenshots](25-blank-or-incomplete-screenshots.md) — or +keep the Hilt tests apart. + +::: + +For more on Hilt's testing APIs, see [Hilt testing guide ](https://developer.android.com/training/dependency-injection/hilt-testing).