Leitfaden für Hilt-Tests

Einer der Vorteile der Verwendung von Frameworks für die Abhängigkeitsinjektion wie Hilt ist, dass das Testen Ihres Codes einfacher wird.

Einheitentests

Hilt ist für Unit-Tests nicht erforderlich, da Sie beim Testen einer Klasse, die die Konstruktorinjektion verwendet, Hilt nicht zum Instanziieren dieser Klasse benötigen. Stattdessen können Sie einen Klassenkonstruktor direkt aufrufen, indem Sie gefälschte oder Mock-Abhängigkeiten übergeben, genau wie wenn der Konstruktor nicht mit einer Annotation versehen wäre:

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

class AnalyticsAdapterTest {

  @Test
  fun `Happy path`() {
    // You don't need Hilt to create an instance of AnalyticsAdapter.
    // You can pass a fake or mock AnalyticsService.
    val adapter = AnalyticsAdapter(fakeAnalyticsService)
    assertEquals(...)
  }
}

Dasselbe gilt für ViewModel-Klassen, die durch Aufrufen von hiltViewModel() in Ihren Composables abgerufen werden. Erstellen Sie das ViewModel in Unittests direkt mit Fakes. Informationen dazu, wie der Status von einem ViewModel in Composables übertragen wird, finden Sie unter Status und Jetpack Compose und Wo wird der Status übertragen?.

End-to-End-Tests

Bei Integrationstests fügt Hilt Abhängigkeiten wie in Ihrem Produktionscode ein. Tests mit Hilt erfordern keine Wartung, da Hilt für jeden Test automatisch einen neuen Satz von Komponenten generiert.

Testabhängigkeiten hinzufügen

Wenn Sie Hilt in Ihren Tests verwenden möchten, fügen Sie die hilt-android-testing-Abhängigkeit in Ihr Projekt ein:

dependencies {
    // For Robolectric tests.
    testImplementation("com.google.dagger:hilt-android-testing:2.57.1")
    kspTest("com.google.dagger:hilt-android-compiler:2.57.1")

    // For instrumented tests.
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.57.1")
    kspAndroidTest("com.google.dagger:hilt-android-compiler:2.57.1")

    // Compose UI test rule.
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

}

UI-Test einrichten

Jeder UI-Test, in dem Hilt verwendet wird, muss mit @HiltAndroidTest annotiert werden. Diese Annotation ist für das Generieren der Hilt-Komponenten für jeden Test verantwortlich.

Außerdem müssen Sie der Testklasse HiltAndroidRule hinzufügen. Sie verwaltet den Status der Komponenten und wird verwendet, um die Abhängigkeitsinjektion in Ihrem Test durchzuführen:

@HiltAndroidTest
class SettingsScreenTest {

    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<HiltTestActivity>()

    // Compose UI tests here.
}

Als Nächstes muss Ihr Test die Klasse Application kennen, die Hilt automatisch für Sie generiert.

Damit Hilt Abhängigkeiten einschleusen kann, müssen Sie im Source-Set androidTest eine leere Activity mit dem Namen HiltTestActivity erstellen und mit @AndroidEntryPoint annotieren. createAndroidComposeRule verwendet diese Aktivität dann als Host für Ihre zusammensetzbaren Inhalte.

App testen

Sie müssen instrumentierte Tests, die Hilt verwenden, in einem Application-Objekt ausführen, das Hilt unterstützt. Die Bibliothek bietet HiltTestApplication für die Verwendung in Tests. Wenn für Ihre Tests eine andere Basisanwendung erforderlich ist, lesen Sie den Abschnitt Benutzerdefinierte Anwendung für Tests.

Sie müssen Ihre Testanwendung so einrichten, dass sie in Ihren instrumentierten Tests oder Robolectric-Tests ausgeführt wird. Die folgende Anleitung ist nicht spezifisch für Hilt, sondern enthält allgemeine Richtlinien dazu, wie Sie eine benutzerdefinierte Anwendung für Tests angeben.

Testanwendung in instrumentierten Tests festlegen

Wenn Sie die Hilt-Testanwendung in instrumentierten Tests verwenden möchten, müssen Sie einen neuen Testrunner konfigurieren. Dadurch funktioniert Hilt für alle instrumentierten Tests in Ihrem Projekt. Führen Sie die folgenden Schritte aus:

  1. Erstellen Sie eine benutzerdefinierte Klasse, die AndroidJUnitRunner im Ordner androidTest erweitert.
  2. Überschreiben Sie die Funktion newApplication und übergeben Sie den Namen der generierten Hilt-Testanwendung.
// A custom runner to set up the instrumented application class for tests.
class CustomTestRunner : AndroidJUnitRunner() {

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

Konfigurieren Sie diesen Test-Runner als Nächstes in Ihrer Gradle-Datei, wie im Leitfaden zu instrumentierten Einheitentests beschrieben. Achten Sie darauf, dass Sie den vollständigen Klassenpfad verwenden:

android {
    defaultConfig {
        // Replace com.example.android.dagger with your class path.
        testInstrumentationRunner = "com.example.android.dagger.CustomTestRunner"
    }
}
Testanwendung in Robolectric-Tests festlegen

Wenn Sie Robolectric zum Testen der UI-Ebene verwenden, können Sie in der robolectric.properties-Datei angeben, welche Anwendung verwendet werden soll:

application = dagger.hilt.android.testing.HiltTestApplication

Alternativ können Sie die Anwendung für jeden Test einzeln konfigurieren, indem Sie die @Config-Annotation von Robolectric verwenden:

@HiltAndroidTest
@Config(application = HiltTestApplication::class)
class SettingsScreenTest {

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  // Robolectric tests here.
}

Testfunktionen

Sobald Hilt in Ihren Tests verwendet werden kann, haben Sie mehrere Möglichkeiten, den Testprozess anzupassen.

Typen in Tests einschleusen

Wenn Sie Typen in einen Test einschleusen möchten, verwenden Sie @Inject für Field Injection. Damit Hilt die @Inject-Felder ausfüllt, rufen Sie hiltRule.inject() auf.

Hier ein Beispiel für einen instrumentierten Test:

@HiltAndroidTest
class SettingsScreenTest {

    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<HiltTestActivity>()

    @Inject
    lateinit var analyticsAdapter: AnalyticsAdapter

    @Before
    fun init() {
        hiltRule.inject()
    }

    @Test
    fun settingsScreen_showsTitle() {
        composeRule.setContent {
            SettingsScreen()
        }
        composeRule.onNodeWithText("Settings").assertIsDisplayed()
        // analyticsRepository is available here.
    }
}

Bindung ersetzen

Wenn Sie eine gefälschte oder Mock-Instanz einer Abhängigkeit einschleusen müssen, müssen Sie Hilt mitteilen, dass die Bindung, die im Produktionscode verwendet wurde, nicht verwendet werden soll und stattdessen eine andere verwendet werden soll. Wenn Sie eine Bindung ersetzen möchten, müssen Sie das Modul, das die Bindung enthält, durch ein Testmodul ersetzen, das die Bindungen enthält, die Sie im Test verwenden möchten.

Angenommen, in Ihrem Produktionscode wird eine Bindung für AnalyticsService so deklariert:

@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

Wenn Sie die AnalyticsService-Bindung in Tests ersetzen möchten, erstellen Sie ein neues Hilt-Modul im Ordner test oder androidTest mit der gefälschten Abhängigkeit und annotieren Sie es mit @TestInstallIn. Stattdessen wird in alle Tests in diesem Ordner die gefälschte Abhängigkeit eingefügt.

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AnalyticsModule::class]
)
abstract class FakeAnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    fakeAnalyticsService: FakeAnalyticsService
  ): AnalyticsService
}

Da Composables diese Abhängigkeiten in der Regel indirekt über ein mit hiltViewModel() abgerufenes ViewModel nutzen, reicht es aus, die Bindung in Hilt zu ersetzen. Das zu testende Composable übernimmt automatisch das Fake.

Bindung in einem einzelnen Test ersetzen

Wenn Sie eine Bindung in einem einzelnen Test anstelle aller Tests ersetzen möchten, deinstallieren Sie ein Hilt-Modul aus einem Test mit der Annotation @UninstallModules und erstellen Sie ein neues Testmodul im Test.

Beginnen Sie mit dem AnalyticsService-Beispiel aus der vorherigen Version und weisen Sie Hilt an, das Produktionsmodul mit der Annotation @UninstallModules in der Testklasse zu ignorieren:

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsScreenTest { ... }

Als Nächstes müssen Sie die Bindung ersetzen. Erstellen Sie ein neues Modul in der Testklasse, in dem die Testbindung definiert wird:

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsScreenTest {

  @Module
  @InstallIn(SingletonComponent::class)
  abstract class TestModule {

    @Singleton
    @Binds
    abstract fun bindAnalyticsService(
      fakeAnalyticsService: FakeAnalyticsService
    ): AnalyticsService
  }

  // ...
}

Dadurch wird nur die Bindung für eine einzelne Testklasse ersetzt. Wenn Sie die Bindung für alle Testklassen ersetzen möchten, verwenden Sie die Annotation @TestInstallIn aus dem Abschnitt oben. Alternativ können Sie die Testbindung für Robolectric-Tests in das Modul test oder für instrumentierte Tests in das Modul androidTest einfügen. Wir empfehlen, nach Möglichkeit immer @TestInstallIn zu verwenden.

Neue Werte binden

Mit der Annotation @BindValue können Sie Felder in Ihrem Test ganz einfach an den Hilt-Abhängigkeitsgraphen binden. Wenn Sie ein Feld mit @BindValue annotieren, wird es unter dem deklarierten Feldtyp mit allen für dieses Feld vorhandenen Qualifizierern gebunden.

Im Beispiel AnalyticsService können Sie AnalyticsService durch ein gefälschtes Bild ersetzen, indem Sie @BindValue verwenden:

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsScreenTest {

  @BindValue @JvmField
  val analyticsService: AnalyticsService = FakeAnalyticsService()

  ...
}

Dadurch wird sowohl das Ersetzen als auch das Referenzieren einer Bindung in Ihrem Test vereinfacht, da Sie beides gleichzeitig ausführen können.

@BindValue funktioniert mit Qualifizierern und anderen Testannotationen. Wenn Sie beispielsweise Testbibliotheken wie Mockito verwenden, können Sie sie in einem Robolectric-Test so verwenden:

...
class SettingsScreenTest {
  ...

  @BindValue @ExampleQualifier @Mock
  lateinit var qualifiedVariable: ExampleCustomType

  // Robolectric tests here
}

Wenn Sie ein Multibinding hinzufügen müssen, können Sie anstelle von @BindValue die Annotationen @BindValueIntoSet und @BindValueIntoMap verwenden. Für @BindValueIntoMap müssen Sie das Feld auch mit einer Karten-Schlüsselanmerkung versehen.

Sonderfälle

Hilt bietet auch Funktionen zur Unterstützung von nicht standardmäßigen Anwendungsfällen.

Benutzerdefinierte Anwendung für Tests

Wenn Sie HiltTestApplication nicht verwenden können, weil Ihre Testanwendung eine andere Anwendung erweitern muss, annotieren Sie eine neue Klasse oder Schnittstelle mit @CustomTestApplication und übergeben Sie den Wert der Basisklasse, die von der generierten Hilt-Anwendung erweitert werden soll.

@CustomTestApplication generiert eine Application-Klasse, die mit Hilt getestet werden kann und die Anwendung erweitert, die Sie als Parameter übergeben haben.

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

Im Beispiel generiert Hilt eine Application namens HiltTestApplication_Application, die die Klasse BaseApplication erweitert. Im Allgemeinen ist der Name der generierten Anwendung der Name der annotierten Klasse mit dem Suffix _Application. Die generierte Hilt-Testanwendung muss in Ihren instrumentierten Tests oder Robolectric-Tests ausgeführt werden, wie unter Testanwendung beschrieben.

Mehrere TestRule-Objekte in Ihrem instrumentierten Test

Compose-UI-Tests kombinieren HiltAndroidRule bereits mit einer Compose-Testregel wie createAndroidComposeRule. Wenn Sie zusätzliche TestRule-Objekte haben, muss HiltAndroidRule zuerst ausgeführt werden. Legen Sie die Ausführungsreihenfolge mit dem Attribut order für @Rule fest:

@HiltAndroidTest
class SettingsScreenTest {

  @get:Rule(order = 0)
  var hiltRule = HiltAndroidRule(this)

  @get:Rule(order = 1)
  val composeRule = createAndroidComposeRule<HiltTestActivity>()

  @get:Rule(order = 2)
  val otherRule = SomeOtherRule()

  // UI tests here.
}

Alternativ können Sie die Regeln mit RuleChain umschließen und HiltAndroidRule als äußere Regel festlegen.

@HiltAndroidTest
class SettingsScreenTest {

  @get:Rule
  var rule = RuleChain.outerRule(HiltAndroidRule(this)).
        around(SettingsScreenTestRule(...))

  // UI tests here.
}

Einen Einstiegspunkt verwenden, bevor die Singleton-Komponente verfügbar ist

Die Annotation @EarlyEntryPoint bietet eine Möglichkeit, einen Hilt-Einstiegspunkt zu erstellen, bevor die Singleton-Komponente in einem Hilt-Test verfügbar ist.

Weitere Informationen zu @EarlyEntryPoint finden Sie in der Hilt-Dokumentation.

Zusätzliche Ressourcen

Weitere Informationen zum Testen finden Sie in den folgenden zusätzlichen Ressourcen:

Dokumentation

Inhalte ansehen