Test-Doubles in Android verwenden

Beim Entwickeln der Teststrategie für ein Element oder System gibt es drei zugehörige Testaspekte:

  • Umfang: Wie viel vom Code wird durch den Test bearbeitet? Tests können eine einzelne Methode, die gesamte Anwendung oder irgendwo dazwischen prüfen. Der getestete Bereich ist wird getestet und wird im Allgemeinen als Testobjekt bezeichnet, aber auch als System Under Test oder Unit Under Test.
  • Geschwindigkeit: Wie schnell wird der Test ausgeführt? Die Testgeschwindigkeiten können von Millisekunden bis mehreren Minuten variieren.
  • Fidelity: Ist der Test "real"? Wenn beispielsweise ein Teil des Codes, den Sie testen, eine Netzwerkanfrage senden muss: Wird diese Netzwerkanfrage durch den Testcode tatsächlich gestellt oder wird das Ergebnis gefälscht? Spricht der Test tatsächlich mit dem Netzwerk, hat er eine höhere Grafikqualität. Der Nachteil: Der Test kann länger dauern, zu Fehlern führen, wenn das Netzwerk ausfällt, oder die Nutzung kostspielig sein.

Unter Was muss getestet werden erfahren Sie, wie Sie Ihre Teststrategie festlegen können.

Isolation und Abhängigkeiten

Wenn Sie ein Element oder System von Elementen testen, tun Sie dies isoliert. Zum Testen eines ViewModel müssen Sie beispielsweise keinen Emulator und keine UI starten, da diese nicht vom Android-Framework abhängig ist (oder auch nicht).

Das Prüfobjekt kann jedoch von anderen abhängen, damit es funktioniert. Beispielsweise kann ein ViewModel von einem Daten-Repository abhängig sein, damit es funktioniert.

Wenn Sie eine Abhängigkeit für ein getestetes Objekt angeben müssen, wird üblicherweise ein Test-Double (oder Testobjekt) erstellt. Test-Doubles sind Objekte, die in Ihrer App als Komponenten aussehen und fungieren. Sie werden jedoch in Ihrem Test erstellt, um ein bestimmtes Verhalten oder bestimmte Daten bereitzustellen. Die Hauptvorteile sind, dass sie Ihre Tests schneller und einfacher machen.

Arten von Test-Doubles

Es gibt verschiedene Arten von Test-Doubles:

Fake Ein Test-Double mit einer "funktionierenden" Implementierung der Klasse, die jedoch so implementiert ist, dass sie gut für Tests, aber nicht für die Produktion geeignet ist.

Beispiel: eine speicherinterne Datenbank.

Fälschungen erfordern kein Mocking-Framework und sind leicht. Sie werden bevorzugt.

Modell Ein Test-Double, das sich so verhält, wie Sie es programmiert, und das Erwartungen an seine Interaktionen hat. Simulationen schlagen die Tests fehl, wenn ihre Interaktionen nicht den von Ihnen definierten Anforderungen entsprechen. Um dies zu erreichen, werden Modelle in der Regel mit einem Mocking-Framework erstellt.

Beispiel: Prüfen, ob eine Methode in einer Datenbank genau einmal aufgerufen wurde.

Stub Ein Test-Double, das sich so verhält, wie Sie es programmieren, aber keine Erwartungen an seine Interaktionen hat. Wird normalerweise mit einem Mocking-Framework erstellt. Der Einfachheit halber werden Fälschungen gegenüber Stubs bevorzugt.
Dummy Ein Test-Double, das weitergegeben, aber nicht verwendet wird, beispielsweise wenn Sie es nur als Parameter angeben müssen.

Beispiel: Eine leere Funktion wird als Klick-Callback übergeben.

Spy – Susan Cooper Undercover Ein Wrapper über einem echten Objekt, der auch einige zusätzliche Informationen verfolgt, ähnlich wie Simulationen. Sie werden in der Regel vermieden, um die Komplexität zu erhöhen. Fälschungen oder Simulationen werden daher gegenüber Agenten bevorzugt.
Shadow Eine Fälschung wird in Robolectric verwendet.

Beispiel für die Verwendung einer Fälschung

Angenommen, Sie möchten einen Einheitentest für ein ViewModel durchführen, das auf einer Schnittstelle namens UserRepository basiert und den Namen des ersten Nutzers in einer UI verfügbar macht. Sie können ein fiktives Test-Double erstellen, indem Sie die Schnittstelle implementieren und bekannte Daten zurückgeben.

object FakeUserRepository : UserRepository {
    fun getUsers() = listOf(UserAlice, UserBob)
}

val const UserAlice = User("Alice")
val const UserBob = User("Bob")

Diese fiktive UserRepository-Instanz muss nicht von den lokalen und Remote-Datenquellen abhängen, die die Produktionsversion verwenden würde. Die Datei befindet sich im Testquellsatz und wird nicht mit der Produktions-App ausgeliefert.

Durch eine unechte Abhängigkeit können bekannte Daten zurückgegeben werden, ohne dass sie sich auf Remote-Datenquellen verlassen müssen.
Abbildung 1: Eine unechte Abhängigkeit in einem Einheitentest.

Mit dem folgenden Test wird bestätigt, dass die ViewModel-Ansicht den ersten Nutzernamen korrekt für die Ansicht verfügbar macht.

@Test
fun viewModelA_loadsUsers_showsFirstUser() {
    // Given a VM using fake data
    val viewModel = ViewModelA(FakeUserRepository) // Kicks off data load on init

    // Verify that the exposed data is correct
    assertEquals(viewModel.firstUserName, UserAlice.name)
}

Das Ersetzen der UserRepository durch eine Fälschung ist in einem Einheitentest einfach, da das ViewModel vom Tester erstellt wird. Es kann jedoch schwierig sein, beliebige Elemente in größeren Tests zu ersetzen.

Komponenten und Abhängigkeitsinjektion ersetzen

Wenn Tests keine Kontrolle über die Erstellung der zu testenden Systeme haben, wird das Ersetzen von Komponenten für Test-Doubles komplizierter und erfordert, dass die Architektur Ihrer Anwendung einem testbaren Design folgt.

Auch große End-to-End-Tests können von Testdoppeln profitieren, z. B. ein instrumentierter UI-Test, der einen vollständigen Nutzerfluss in Ihrer App durchläuft. In diesem Fall sollten Sie Ihren Test hermetisch machen. Bei einem hermetischen Test werden alle externen Abhängigkeiten, z. B. das Abrufen von Daten aus dem Internet, vermieden. Dies verbessert die Zuverlässigkeit und Leistung.

Abbildung 2: Ein großer Test, der den größten Teil der App abdeckt und Remote-Daten fälscht.

Sie können diese Flexibilität auch manuell erzielen, aber wir empfehlen die Verwendung eines Abhängigkeitsinjektions-Frameworks wie Hilt, um Komponenten in Ihrer App zur Testzeit zu ersetzen. Weitere Informationen finden Sie im Leitfaden zu Hilt-Tests.

Robolektisch

Unter Android können Sie das Robolectric-Framework verwenden, das einen speziellen Test-Double-Typ bietet. Mit Robolectric können Sie Tests auf Ihrer Workstation oder in Ihrer Continuous-Integration-Umgebung ausführen. Dabei wird eine reguläre JVM ohne Emulator oder Gerät verwendet. Es simuliert die Inflation von Ansichten, das Laden von Ressourcen und anderen Teilen des Android-Frameworks mit Test-Doubles, den sogenannten Schatten.

Robolectric ist ein Simulator und sollte daher keine einfachen Unittests ersetzen und nicht für Kompatibilitätstests verwendet werden. Sie sorgt für Geschwindigkeit und senkt die Kosten, allerdings auf Kosten einer geringeren Genauigkeit. Ein guter Ansatz für UI-Tests besteht darin, sie sowohl mit Robolectric- als auch instrumentierten Tests kompatibel zu machen und zu entscheiden, wann sie ausgeführt werden sollen, je nachdem, ob Funktionen oder Kompatibilität getestet werden müssen. Auf Robolectric können sowohl Espresso- als auch Compose-Tests ausgeführt werden.