Dieser Artikel ist der letzte Teil einer Reihe zur Anwendung des MVI-Architekturmusters in Kotlin Multiplatform. In den beiden vorherigen Teilen ( Teil 1 und Teil 2 ) haben wir uns daran erinnert, was MVI ist, ein generisches Kittens-Modul zum Laden von Katzenbildern erstellt und es in iOS- und Android-Anwendungen integriert.
In diesem Teil behandeln wir das Kittens-Modul mit Unit- und Integrationstests. Wir werden die aktuellen Einschränkungen beim Testen in Kotlin Multiplatform kennenlernen, herausfinden, wie sie überwunden werden können, und sie sogar zu unserem Vorteil einsetzen.
Ein aktualisiertes Beispielprojekt ist auf unserem GitHub verfügbar .
Prolog
Es besteht kein Zweifel, dass das Testen ein wichtiger Schritt in der Softwareentwicklung ist. Natürlich verlangsamt es den Prozess, aber gleichzeitig:
- Mit dieser Option können Sie Kantenfälle überprüfen, die nur schwer manuell zu erfassen sind.
- Reduziert die Wahrscheinlichkeit einer Regression beim Hinzufügen neuer Funktionen, Beheben von Fehlern und Refactoring.
- zwingt Sie, Ihren Code zu zerlegen und zu strukturieren.
Auf den ersten Blick mag der letzte Punkt als Nachteil erscheinen, weil er Zeit braucht. Dies macht den Code jedoch auf lange Sicht lesbarer und vorteilhafter.
„In der Tat liegt das Verhältnis von Lese- und Schreibzeit weit über 10 zu 1. Wir lesen ständig alten Code, um neuen Code zu schreiben. ... [Deshalb] erleichtert das Lesen das Schreiben. " - Robert C. Martin, "Clean Code: Ein Handbuch für agile Software-Handwerkskunst"
Kotlin Multiplatform erweitert die Testfunktionen. Diese Technologie fügt eine wichtige Funktion hinzu: Jeder Test wird automatisch auf allen unterstützten Plattformen durchgeführt. Wenn beispielsweise nur Android und iOS unterstützt werden, kann die Anzahl der Tests mit zwei multipliziert werden. Und wenn irgendwann Unterstützung für eine andere Plattform hinzugefügt wird, wird diese automatisch in Tests behandelt.
Das Testen auf allen unterstützten Plattformen ist wichtig, da es Unterschiede im Verhalten des Codes geben kann. Zum Beispiel hat Kotlin / Native ein spezielles Speichermodell , Kotlin / JS liefert manchmal auch unerwartete Ergebnisse.
Bevor wir weiter gehen, sollten einige der Testbeschränkungen in Kotlin Multiplatform erwähnt werden. Das größte Problem ist das Fehlen einer Spottbibliothek für Kotlin / Native und Kotlin / JS. Dies mag als großer Nachteil erscheinen, aber ich persönlich betrachte es als Vorteil. Das Testen in Kotlin Multiplatform war für mich ziemlich schwierig: Ich musste Schnittstellen für jede Abhängigkeit erstellen und ihre Testimplementierungen (Fälschungen) schreiben. Es hat lange gedauert, aber irgendwann wurde mir klar, dass das Verbringen von Zeit für Abstraktionen eine Investition ist, die zu saubererem Code führt.
Mir ist auch aufgefallen, dass spätere Änderungen an diesem Code weniger Zeit in Anspruch nehmen. Warum so? Weil die Interaktion einer Klasse mit ihren Abhängigkeiten nicht genagelt (verspottet) wird. In den meisten Fällen reicht es aus, die Testimplementierungen einfach zu aktualisieren. Es ist nicht erforderlich, sich eingehend mit den einzelnen Testmethoden zu befassen, um die Mocks zu aktualisieren. Infolgedessen habe ich die Verwendung von Spottbibliotheken auch in der Standard-Android-Entwicklung eingestellt. Ich empfehle den folgenden Artikel zu lesen: " Verspotten ist nicht praktikabel - Fälschungen verwenden " von Pravin Sonawane .
Planen
Erinnern wir uns, was wir im Kittens-Modul haben und was wir testen sollten.
- KittenStore ist die Hauptkomponente des Moduls. Die KittenStoreImpl- Implementierung enthält den größten Teil der Geschäftslogik. Dies ist das erste, was wir testen werden.
- KittenComponent ist die Modulfassade und der Integrationspunkt für alle internen Komponenten. Wir werden diese Komponente mit Integrationstests abdecken.
- KittenView ist eine öffentliche Schnittstelle, die die UI-Abhängigkeit von KittenComponent darstellt.
- KittenDataSource ist eine interne Webzugriffsschnittstelle mit plattformspezifischen Implementierungen für iOS und Android.
Zum besseren Verständnis der Struktur des Moduls werde ich sein UML-Diagramm geben:
Der Plan lautet wie folgt:
- KittenStore testen
- Erstellen einer Testimplementierung von KittenStore.Parser
- Erstellen einer Testimplementierung von KittenStore.Network
- Unit-Tests für KittenStoreImpl schreiben
- Erstellen einer Testimplementierung von KittenStore.Parser
- Testen der KittenComponent
- Erstellen einer Testimplementierung von KittenDataSource
- Erstellen Sie eine Test-KittenView-Implementierung
- Schreiben von Integrationstests für KittenComponent
- Erstellen einer Testimplementierung von KittenDataSource
- Ausführen von Tests
- Schlussfolgerungen
KittenStore Unit Testing
Die KittenStore-Schnittstelle verfügt über eine eigene Implementierungsklasse - KittenStoreImpl. Das werden wir testen. Es hat zwei Abhängigkeiten (interne Schnittstellen), die direkt in der Klasse selbst definiert sind. Beginnen wir damit, Testimplementierungen für sie zu schreiben.
Testen Sie die Implementierung von KittenStore.Parser
Diese Komponente ist für Netzwerkanforderungen verantwortlich. So sieht die Benutzeroberfläche aus:
Bevor wir eine Testimplementierung einer Netzwerkschnittstelle schreiben, müssen wir eine wichtige Frage beantworten: Welche Daten gibt der Server zurück? Die Antwort ist, dass der Server einen zufälligen Satz von Bildverknüpfungen zurückgibt, jedes Mal einen anderen Satz. Im wirklichen Leben wird das JSON-Format verwendet, aber da wir eine Parser-Abstraktion haben, ist uns das Format in Unit-Tests egal.
Eine echte Implementierung kann Streams wechseln, sodass Abonnenten in Kotlin / Native eingefroren werden können . Es wäre großartig, dieses Verhalten zu modellieren, um sicherzustellen, dass der Code alles korrekt verarbeitet.
Daher sollte unsere Testimplementierung von Network die folgenden Funktionen aufweisen:
- muss für jede Anforderung einen nicht leeren Satz verschiedener Zeilen zurückgeben;
- Das Antwortformat muss für Netzwerk und Parser gleich sein.
- sollte in der Lage sein, Netzwerkfehler zu simulieren (sollte möglicherweise ohne Antwort abgeschlossen werden);
- Es muss möglich sein, ein ungültiges Antwortformat zu simulieren (um im Parser nach Fehlern zu suchen).
- Es sollte möglich sein, Antwortverzögerungen zu simulieren (um die Startphase zu testen).
- sollte in Kotlin / Native einfrierbar sein (nur für den Fall).
Die Testimplementierung selbst könnte folgendermaßen aussehen:
TestKittenStoreNetwork verfügt über einen Zeichenfolgenspeicher (genau wie ein echter Server) und kann Zeichenfolgen generieren. Für jede Anforderung wird die aktuelle Liste der Zeilen in eine Zeile codiert. Wenn die Eigenschaft "images" Null ist, wird "Vielleicht" einfach beendet, was als Fehler angesehen werden sollte.
Wir haben auch TestScheduler verwendet . Dieser Scheduler hat eine wichtige Funktion: Er friert alle eingehenden Aufgaben ein. Somit friert der ObservOn-Operator, der in Verbindung mit dem TestScheduler verwendet wird, den Downstream-Stream sowie alle durch ihn fließenden Daten wie im realen Leben ein. Gleichzeitig wird Multithreading nicht erforderlich sein, was das Testen vereinfacht und zuverlässiger macht.
Darüber hinaus verfügt TestScheduler über einen speziellen "manuellen Verarbeitungsmodus", mit dem wir die Netzwerklatenz simulieren können.
Testen Sie die Implementierung von KittenStore.Parser
Diese Komponente ist für das Parsen der Antworten vom Server verantwortlich. Hier ist seine Schnittstelle:
Was auch immer aus dem Web heruntergeladen wird, sollte in eine Liste von Links konvertiert werden. Unser Netzwerk verkettet Zeichenfolgen einfach mit einem Semikolon (;) - Trennzeichen. Verwenden Sie daher hier dasselbe Format.
Hier ist eine Testimplementierung:
Wie bei Network wird TestScheduler verwendet, um Abonnenten einzufrieren und ihre Kompatibilität mit dem Kotlin / Native-Speichermodell zu überprüfen. Antwortverarbeitungsfehler werden simuliert, wenn die Eingabezeichenfolge leer ist.
Unit-Tests für KittenStoreImpl
Wir haben jetzt Testimplementierungen aller Abhängigkeiten. Es ist Zeit für Unit-Tests. Alle Unit-Tests finden Sie im Repository , hier werde ich nur die Initialisierung und einige Tests selbst geben.
Der erste Schritt besteht darin, Instanzen unserer Testimplementierungen zu erstellen:
KittenStoreImpl verwendet mainScheduler. Der nächste Schritt besteht darin, es zu überschreiben:
Jetzt können wir einige Tests durchführen. KittenStoreImpl sollte Bilder sofort nach der Erstellung laden. Dies bedeutet, dass eine Netzwerkanforderung abgeschlossen sein muss, ihre Antwort verarbeitet werden muss und der Status mit dem neuen Ergebnis aktualisiert werden muss.
Was wir gemacht haben:
- generierte Bilder im Netzwerk;
- hat eine neue Instanz von KittenStoreImpl erstellt;
- Stellen Sie sicher, dass der Status die richtige Liste von Zeichenfolgen enthält.
Ein weiteres Szenario, das wir berücksichtigen müssen, ist das Abrufen von KittenStore.Intent.Reload. In diesem Fall muss die Liste aus dem Netzwerk neu geladen werden.
Testschritte:
- Quellbilder erzeugen;
- Erstellen Sie eine Instanz von KittenStoreImpl.
- neue Bilder erzeugen;
- send Intent.Reload;
- Stellen Sie sicher, dass die Bedingung neue Bilder enthält.
Schauen wir uns zum Schluss das folgende Szenario an: Wenn das isLoading-Flag gesetzt ist, während Bilder geladen werden.
Wir haben die manuelle Verarbeitung für TestScheduler aktiviert - jetzt werden Aufgaben nicht automatisch verarbeitet. Auf diese Weise können wir den Status überprüfen, während wir auf eine Antwort warten.
KittenComponent Integrationstest
Wie oben erwähnt, ist KittenComponent der Integrationspunkt des gesamten Moduls. Wir können es mit Integrationstests abdecken. Werfen wir einen Blick auf die API:
Es gibt zwei Abhängigkeiten, KittenDataSource und KittenView. Wir benötigen hierfür Testimplementierungen, bevor wir mit dem Testen beginnen können.
Der Vollständigkeit halber zeigt dieses Diagramm den Datenfluss innerhalb des Moduls:
Testen Sie die Implementierung von KittenDataSource
Diese Komponente ist für Netzwerkanforderungen verantwortlich. Es gibt separate Implementierungen für jede Plattform, und wir benötigen eine andere Implementierung für die Tests. So sieht die KittenDataSource-Oberfläche aus:
TheCatAPI unterstützt die Paginierung, daher habe ich sofort die entsprechenden Argumente hinzugefügt. Ansonsten ist es KittenStore.Network sehr ähnlich, das wir zuvor implementiert haben. Der einzige Unterschied besteht darin, dass wir das JSON-Format verwenden müssen, wenn wir echten Code in der Integration testen. Wir leihen uns also nur die Implementierungsidee aus:
Nach wie vor generieren wir verschiedene Listen von Zeichenfolgen, die bei jeder Anforderung in ein JSON-Array codiert werden. Wenn keine Bilder generiert werden oder die Anforderungsargumente falsch sind, wird Vielleicht nur ohne Antwort beendet.
Die Bibliothek kotlinx.serialization wird zum Bilden eines JSON-Arrays verwendet . Der getestete KittenStoreParser verwendet es übrigens zum Dekodieren.
Testimplementierung von KittenView
Dies ist die letzte Komponente, für die wir eine Testimplementierung benötigen, bevor wir mit dem Testen beginnen können. Hier ist seine Schnittstelle:
Diese Ansicht nimmt nur Modelle auf und löst Ereignisse aus. Die Testimplementierung ist daher sehr einfach:
Wir müssen uns nur das zuletzt akzeptierte Modell merken - so können wir die Richtigkeit des angezeigten Modells überprüfen. Wir können Ereignisse auch im Auftrag von KittenView mithilfe der Versandmethode (Ereignis) auslösen, die in der geerbten AbstractMviView-Klasse deklariert ist.
Integrationstests für KittenComponent
Die vollständigen Tests finden Sie im Repository . Hier werde ich nur einige der interessantesten nennen.
Beginnen wir wie zuvor damit, Abhängigkeiten zu instanziieren und zu initialisieren:
Derzeit werden zwei Scheduler für das Modul verwendet: mainScheduler und computationScheduler. Wir müssen sie überschreiben:
Wir können jetzt einige Tests schreiben. Lassen Sie uns zuerst das Hauptskript überprüfen, um sicherzustellen, dass die Bilder beim Start geladen und angezeigt werden:
Dieser Test ist dem sehr ähnlich, den wir geschrieben haben, als wir uns die Unit-Tests für KittenStore angesehen haben. Erst jetzt ist das gesamte Modul beteiligt.
Testschritte:
- Generieren Sie Links zu Bildern in TestKittenDataSource.
- KittenComponent erstellen und ausführen;
- Stellen Sie sicher, dass die Links TestKittenView erreichen.
Ein weiteres interessantes Szenario: Bilder müssen neu geladen werden, wenn KittenView das RefreshTriggered-Ereignis auslöst.
Stufen:
- Quelllinks zu Bildern generieren;
- KittenComponent erstellen und ausführen;
- neue Links generieren;
- send Event.RefreshTriggered im Auftrag von KittenView;
- Stellen Sie sicher, dass neue Links TestKittenView erreichen.
Ausführen von Tests
Um alle Tests auszuführen, müssen wir die folgende Gradle-Aufgabe ausführen:
./gradlew :shared:kittens:build
Dadurch wird das Modul kompiliert und alle Tests auf allen unterstützten Plattformen ausgeführt: Android und iosx64.
Und hier ist der JaCoCo-Bericht:
Fazit
In diesem Artikel haben wir das Kittens-Modul mit Unit- und Integrationstests behandelt. Das vorgeschlagene Moduldesign ermöglichte es uns, die folgenden Teile abzudecken:
- KittenStoreImpl - enthält den größten Teil der Geschäftslogik;
- KittenStoreNetwork - verantwortlich für Netzwerkanforderungen auf hoher Ebene;
- KittenStoreParser - verantwortlich für das Parsen von Netzwerkantworten;
- alle Transformationen und Verbindungen.
Der letzte Punkt ist sehr wichtig. Eine Abdeckung ist dank der MVI-Funktion möglich Es liegt in der alleinigen Verantwortung der Ansicht, Daten und Versandereignisse anzuzeigen. Alle Abonnements, Konvertierungen und Links erfolgen innerhalb des Moduls. Somit können wir mit allgemeinen Tests alles außer dem Display selbst abdecken.
Solche Tests haben folgende Vorteile:
- Verwenden Sie keine Plattform-APIs.
- sehr schnell durchgeführt;
- zuverlässig (nicht blinken);
- auf allen unterstützten Plattformen ausführen.
Wir konnten den Code auch auf Kompatibilität mit dem komplexen Kotlin / Native-Speichermodell testen. Dies ist auch wegen der mangelnden Sicherheit zur Erstellungszeit sehr wichtig: Der Code stürzt nur zur Laufzeit mit Ausnahmen ab, die schwer zu debuggen sind.
Hoffe das hilft dir bei deinen Projekten. Vielen Dank für das Lesen meiner Artikel! Und vergiss nicht, mir auf Twitter zu folgen .
...
Bonusübung
Wenn Sie mit Testimplementierungen arbeiten oder mit MVI spielen möchten, finden Sie hier einige praktische Übungen.
Refactoring der KittenDataSource
Das Modul enthält zwei Implementierungen der KittenDataSource-Schnittstelle: eine für Android und eine für iOS. Ich habe bereits erwähnt, dass sie für den Netzwerkzugriff verantwortlich sind. Sie haben jedoch tatsächlich eine andere Funktion: Sie generieren die URL für die Anforderung basierend auf den Eingabeargumenten "limit" und "page". Gleichzeitig haben wir eine KittenStoreNetwork-Klasse, die nichts anderes tut, als den Aufruf an KittenDataSource zu delegieren.
Herausforderung: Verschieben Sie die Logik zur Generierung von URL-Anforderungen von KittenDataSourceImpl (unter Android und iOS) nach KittenStoreNetwork. Sie müssen die KittenDataSource-Oberfläche wie folgt ändern:
Sobald Sie dies getan haben, müssen Sie Ihre Tests aktualisieren. Die einzige Klasse, die Sie berühren müssen, ist TestKittenDataSource.
Seitenladen hinzufügen
TheCatAPI unterstützt die Paginierung, sodass wir diese Funktionalität für eine bessere Benutzererfahrung hinzufügen können. Sie können beginnen, indem Sie ein neues Event.EndReached-Ereignis für KittenView hinzufügen. Danach wird der Code nicht mehr kompiliert. Anschließend müssen Sie das entsprechende Intent.LoadMore hinzufügen, das neue Ereignis in einen Intent konvertieren und diesen in KittenStoreImpl verarbeiten. Sie müssen auch die KittenStoreImpl.Network-Schnittstelle wie folgt ändern:
Schließlich müssen Sie einige Testimplementierungen aktualisieren, einen oder zwei vorhandene Tests korrigieren und dann einige neue schreiben, um die Paginierung abzudecken.
