Leitfaden für Hilt-Tests

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

Einheitentests

Hilt ist für Unit-Tests nicht erforderlich, da Sie beim Testen einer Klasse, die Constructor Injection verwendet, Hilt nicht zum Instanziieren dieser Klasse verwenden müssen. Stattdessen können Sie einen Klassenkonstruktor direkt aufrufen, indem Sie gefälschte oder simulierte Abhängigkeiten übergeben, so als wäre der Konstruktor nicht annotiert:

@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 fließt, finden Sie unter Status und Jetpack Compose und Wo der Status hochgezogen werden soll.

End-to-End-Tests

Bei Integrationstests fügt Hilt Abhängigkeiten wie im 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 Abhängigkeit hilt-android-testing 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")

}

Einrichtung des UI-Tests

Sie müssen jeden UI-Test, der Hilt verwendet, mit @HiltAndroidTest annotieren. Diese Annotation ist für die Generierung der Hilt-Komponenten für jeden Test verantwortlich.

Außerdem müssen Sie die HiltAndroidRule zur Testklasse hinzufügen. Sie verwaltet den Status der Komponenten und wird verwendet, um die Injektion in Ihren 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 einfügen kann, müssen Sie eine leere Aktivität mit dem Namen HiltTestActivity in Ihrem androidTest-Source-Set erstellen und mit @AndroidEntryPoint annotieren. createAndroidComposeRule verwendet diese Aktivität dann als Host für Ihre zusammensetzbaren Inhalte.

App testen

Sie müssen Instrumentierungstests, die Hilt verwenden, in einem Application-Objekt ausführen, das Hilt unterstützt. Die Bibliothek stellt HiltTestApplication für die Verwendung in Tests bereit. Wenn für Ihre Tests eine andere Basisanwendung erforderlich ist, finden Sie weitere Informationen unter 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 zum Angeben einer benutzerdefinierten Anwendung, die in Tests ausgeführt werden soll.

Testanwendung in Instrumentierungstests festlegen

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

  1. Erstellen Sie im Ordner androidTest eine benutzerdefinierte Klasse, die AndroidJUnitRunner 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)
    }
}

Konfiguriere diesen Test-Runner als Nächstes in deiner Gradle-Datei, wie im Leitfaden für instrumentierte Unittests beschrieben. Achten Sie darauf, den vollständigen Klassenpfad zu 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 Ihrer UI-Ebene verwenden, können Sie in der Datei robolectric.properties angeben, welche Anwendung verwendet werden soll:

application = dagger.hilt.android.testing.HiltTestApplication

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

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

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  // Robolectric tests here.
}

Testfunktionen

Sobald Hilt für Ihre Tests bereit ist, können Sie den Testprozess mit mehreren Funktionen anpassen.

Typen in Tests einfügen

Wenn Sie Typen in einen Test einfügen möchten, verwenden Sie @Inject für die Field Injection. Rufen Sie hiltRule.inject() auf, um Hilt anzuweisen, die @Inject-Felder zu füllen.

Hier ein Beispiel für einen Instrumentierungstest:

@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 simulierte Instanz einer Abhängigkeit einfügen müssen, müssen Sie Hilt anweisen, nicht die Bindung zu verwenden, die im Produktionscode verwendet wurde, sondern eine andere. 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, Ihr Produktionscode deklariert eine Bindung für AnalyticsService so:

@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 im Ordner test oder androidTest ein neues Hilt-Modul mit der Fake-Abhängigkeit und fügen Sie die Annotation @TestInstallIn hinzu. Stattdessen wird allen Tests in diesem Ordner die Fake-Abhängigkeit eingefügt.

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

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

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

Bindung in einem einzelnen Test ersetzen

Wenn Sie eine Bindung in einem einzelnen Test statt in allen 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 nach dem AnalyticsService-Beispiel aus der vorherigen Version damit, Hilt anzuweisen, das Produktionsmodul zu ignorieren. Verwenden Sie dazu die Annotation @UninstallModules in der Testklasse:

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

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

@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 obigen Abschnitt. Alternativ können Sie die Testbindung für Robolectric-Tests im Modul test oder für Instrumentierungstests im Modul androidTest platzieren. Es wird empfohlen, nach Möglichkeit @TestInstallIn zu verwenden.

Neue Werte binden

Mit der Annotation @BindValue können Sie Felder in Ihrem Test ganz einfach in das Hilt-Abhängigkeitsdiagramm einbinden. 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 Fake ersetzen, indem Sie @BindValue verwenden:

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

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

  ...
}

Dadurch wird sowohl das Ersetzen einer Bindung als auch das Verweisen auf eine Bindung in Ihrem Test vereinfacht, da Sie beides gleichzeitig tun können.

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

...
class SettingsScreenTest {
  ...

  @BindValue @ExampleQualifier @Mock
  lateinit var qualifiedVariable: ExampleCustomType

  // Robolectric tests here
}

Wenn Sie eine Mehrfachbindung hinzufügen müssen, können Sie die Annotationen @BindValueIntoSet und @BindValueIntoMap anstelle von @BindValue verwenden. Für @BindValueIntoMap müssen Sie das Feld auch mit einer MapKey-Annotation versehen.

Sonderfälle

Hilt bietet auch Funktionen zur Unterstützung nicht standardmäßiger Anwendungsfälle.

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 die generierte Hilt-Anwendung erweitern 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 mit dem Namen HiltTestApplication_Application, die die Klasse BaseApplication erweitert. Im Allgemeinen ist der Name der generierten Anwendung der Name der annotierten Klasse, an den _Application angehängt wird. Sie müssen die generierte Hilt-Testanwendung so einrichten, dass sie in Ihren Instrumentierungstests oder Robolectric-Tests ausgeführt wird, wie unter Testanwendung beschrieben.

Mehrere TestRule-Objekte in Ihrem Instrumentierungstest

In Compose-UI-Tests wird HiltAndroidRule bereits mit einer Compose-Testregel wie createAndroidComposeRule kombiniert. Wenn Sie zusätzliche TestRule-Objekte haben, achten Sie darauf, dass HiltAndroidRule zuerst ausgeführt wird. Deklarieren Sie die Ausführungsreihenfolge mit dem Attribut order für @Rule:

@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 platzieren.

@HiltAndroidTest
class SettingsScreenTest {

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

  // UI tests here.
}

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 findest du in der Hilt-Dokumentation.

Zusätzliche Ressourcen

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

Dokumentation

Inhalte ansehen