Inhalt
- Ein kurzer Überblick über MVVM
- Die Testperson
- Spieleliste Modell anzeigen
- Testumgebung vorbereiten
- Beginnen wir mit dem Testen
- Fazit
Ich habe großes Interesse daran, verschiedene Lösungen für ein Problem zu finden. Derzeit werden verschiedene Architekturen für iOS-Anwendungen untersucht.
Ein kurzer Überblick über MVVM
In meinem vorherigen Artikel "MVVM eine Verbesserung von MVC" Wir haben gesehen, wie MVVM den Ansichtsstatus vom Controller in ViewModel trennt. Wir können View-Zustände, die durch ViewModel dargestellt werden, isoliert steuern. Dies bietet uns die Möglichkeit, Testfälle zu schreiben, um unterschiedliche Verhaltensweisen von View zu überprüfen.
Die Testperson
In diesem Tutorial testen wir die Anwendung "Videospiele". Es ist eine einfache App, die eine Liste von Spielen auf dem Startbildschirm lädt und anzeigt. Unser App-Flow sieht wie folgt aus:
- Die App startet das Laden von Games on View.
- Während des Ladens zeigt die App einen Drehfeld auf dem Bildschirm an.
- Wenn Daten erfolgreich geladen werden, werden Elemente in der Tabellenansicht angezeigt.
- Wenn das Laden mit einem Fehler endet, zeigt die App "Spiele können nicht geladen werden. Bitte versuchen Sie es erneut!" Botschaft.
- Wenn die Anzahl der geladenen Gegenstände 0 ist, zeigt die App "Derzeit keine Spiele verfügbar" an. Botschaft.
Ich habe ein Starterprojekt zusammengestellt, damit Sie im weiteren Verlauf üben können. Laden Sie das Projekt von hier herunter und öffnen Sie das Projekt im Starter-Ordner, um loszulegen.
Spieleliste Modell anzeigen
Bevor wir mit dem Schreiben von Testfällen für unser GamesListViewModel beginnen, werfen wir einen Blick auf dessen Verhalten. Sie finden GamesListViewModel im Stammverzeichnis von Starter Porject.
öffentliches Protokoll GamesListViewModelDelegate {func errorDidOccur (vm: GamesListViewModel) func didStartLoading (vm: GamesListViewModel) func itemsLoaded (vm: GamesListViewModel)} öffentliche Klasse GamesListViewModel {/// Benachrichtigt über unterschiedliche Flow-Ereignisse /// Represetns gibt es einen Fehler, der angezeigt werden soll /// Richtig, wenn ja, sonst Falsch public var showError: Bool {get} /// Fehlermeldung, die auf dem Bildschirm angezeigt werden soll public var errorMessage: String {get} /// Number Anzahl der Elemente in der Liste der geladenen Spiele public var itemsCount: Int {get} /// Stellt dar, ob das Ladezeichen angezeigt wird oder nicht. public var showLoading: Bool {get} /// Diese Methode initiiert die Ladeanforderung an DataService public func loadGames () /// Gibt das Elementansichtsmodell zurück, das in der Liste public func getItem (am Index: Int) angezeigt werden soll -> GameListItemViewModel? /// Nimmt den Datendienst, um Datenoperationen auszuführen public init (_ dataService: DataService)}
In GameListViewModel stellen wir alle Statusinformationen, Statusänderungsbenachrichtigungen (GameListViewModelDelegate) und Aktionen bereit, die die Benutzeroberfläche initiieren kann. GameListViewModel hat eine Abhängigkeit, nämlich DataService, mit der wir die Spieleliste laden können.
Testumgebung vorbereiten
Eines der Unit-Testing-Prinzipien besteht darin, die Testperson zu isolieren und ihre Abhängigkeiten zu simulieren, um alle möglichen Fälle zu testen.
Unser Testobjekt GameListViewModel hat eine Abhängigkeit, d. H. DataService. Um die Umgebung von GameListViewModel zu isolieren und zu simulieren, müssen wir das Verhalten von DataService verspotten. Im Folgenden finden Sie den Mock-Servicer, den wir verwenden werden:
Protokoll DataService {func loadGames (_ Vervollständigung: @escaping ([Game]?, Fehler?) -> Void)} struct MockDataService: DataService {var games: [Game]? var error: Fehler? func loadGames (_ Vervollständigung: @escaping ([Spiel]?, Fehler?) -> Leere) {DispatchQueue.main.async {Wache let games = self.games else {Vervollständigung (nil, self.error!) return} Vervollständigung ( Spiele, nil)}}}
Das GameListViewModel benachrichtigt Ereignisse über GameListViewModelDelegate. Um diese Ereignisse zu überwachen, verwenden wir Folgendes:
öffentliche Struktur MonitorGamesListViewModelDelegate: GamesListViewModelDelegate {public var errorDidOccurCallback: ((_ vm: GamesListViewModel) -> Void)? public var didStartLoadingCallback: ((_ vm: GamesListViewModel) -> Void)? public var itemsLoadedCallback: ((_ vm: GamesListViewModel) -> Void)? public func errorDidOccur (vm: GamesListViewModel) {errorDidOccurCallback? (vm)} public func didStartLoading (vm: GamesListViewModel) {didStartLoadingCallback? (vm)} public func itemsLoaded (vm: GamesListViewModel)
Beginnen wir mit dem Testen
In unserem ersten Testfall testen wir das Verhalten von GamesListViewModel, wenn Daten erfolgreich ohne leere Liste geladen wurden. Zu diesem Zweck weisen wir MockDataService an, eine Spieleliste mit einem Element zu senden, und überprüfen die GamesListViewModel-Antwort mithilfe von MonitorGamesListViewModelDelegate.
/// Nach erfolgreichem Laden der Spieleliste /// Erwartetes Verhalten: /// - Nur das Laden der Show sollte wahr sein und das geladene Laden sollte aufgerufen werden /// - Die geladenen Elemente sollten aufgerufen werden und die Anzahl der Elemente sollte korrekt sein. Func testDataLoadSuccessfully () { // Erwartungen einrichten let gestartetLoadingExpectation = Erwartung (Beschreibung: "Rückruf wurde geladen") let itemsLoadedExpectation = Erwartung (Beschreibung: "Erwartung der Elemente geladen") // Simulation der Umgebung mockDataService.error = nil mockDataService.games = [Spiel (Titel : "Happy life")] // Einrichten von Antwortmonitoren responseMonitor.didStartLoadingCallback = {vm in XCTAssert (vm.showLoading, "Ladeflag sollte wahr sein") XCTAssert (! Vm.showError, "Fehlerflag sollte nicht wahr sein") startLoadingExpectation.fulfill ()} responseMonitor.errorDidOccurCallback = {_ in XCTAssert (false, "Ungültiger Fehlerrückruf")} responseMonitor.itemsLoadedCallback = {vm in XCTAssert (! vm.showLoading, "Ladeflag sollte nicht wahr sein") XTA! vm.showError, "Fehlerflag sollte nicht wahr sein") XCTAssert (vm.itemsCount == 1, "Ungültige Elemente geladen") itemsLoadedExpectation.fulfill ()} // Aktion ausführen vm.loadGames () // auf Erwartungswarte prüfen ( für: [startLoadingExpectation, itemsLoadedExpectation], Zeitüberschreitung: 1 ,forceOrder: true)}
In diesem Testfall führen wir fünf Schritte aus:
- Definierte Erwartungen, die vor dem Beenden des Tests erfüllt werden sollten.
- Einrichten der Abhängigkeiten zum Senden einer gültigen Antwort ohne leere Spieleliste.
- Einrichten des Antwortmonitors, der Ereignisse abhört und den erwarteten Status von ViewModel bestätigt.
- Simulation der Aktion "loadGames" in ViewModel.
- Warten auf die Erfüllung der Erwartungen in einer bestimmten Reihenfolge.
Jetzt werden wir testen, was passiert, wenn das Laden der Datenanforderung mit einem Fehler fehlschlägt. Gemäß unserer Anforderung sollte showError true sein und errorMessage sollte "Spiele können nicht geladen werden. Bitte versuchen Sie es erneut!"
/// Das Laden der Spieleliste endet mit dem Fehler func testDataLoadedWithError () {// Einrichten der Erwartung let StartingLoadingExpectation = Erwartung (Beschreibung: "Rückruf wird geladen.") Let errorOccuredExpectation = Erwartung (Beschreibung: "Fehler beim Laden laden" Simulation der Umgebung mockDataService.error = AppDataServiceError.invalidResponse mockDataService.games = nil // Einrichten von Antwortmonitoren responseMonitor.didStartLoadingCallback = {vm in XCTAssert (vm.showLoading, "Ladeflag sollte wahr sein.") "Fehlerflag sollte nicht wahr sein.") StartedLoadingExpectation.fulfill ()} responseMonitor.errorDidOccurCallback = {vm in XCTAssert (! Vm.showLoading, "Ladeflag sollte nicht wahr sein.") XCTAssert (vm.showError, "Fehlerflag sollte sei wahr. ") XCTAssert (vm.errorMessage ==" Spiele können nicht geladen werden.Bitte versuchen Sie es erneut! "," Ungültige Fehlermeldung. ") XCTAssert (vm.itemsCount == 0," Anzahl der Elemente sollte 0 sein. ") ErrorOccuredExpectation.fulfill ()} responseMonitor.itemsLoadedCallback = {_ in XCTAssert (false," Elemente Der geladene Rückruf sollte nicht aufgerufen werden. ")} // Aktion vm.loadGames () ausführen // auf Erwartungswarte prüfen (auf: [StartLoadingExpectation, errorOccuredExpectation], Zeitüberschreitung: 1, EnforceOrder: true)}
Jetzt ist unser letzter Fall, dass die Daten erfolgreich geladen wurden, die Spieleliste jedoch leer ist. Das gewünschte Ergebnis in diesem Fall ist, dass showError true sein sollte und errorMessage sagen sollte, dass derzeit keine Spiele verfügbar sind.
/// Geladene Spieleliste ist leer func testDataLoadedWithEmptyList () {// Erwartung einrichten let startLoadingExpectation = Erwartung (Beschreibung: "Rückruf wird geladen.") Let errorOccuredExpectation = Erwartung (Beschreibung: "Ladefehlererwartung.") // simulieren Die Umgebung mockDataService.error = nil mockDataService.games = [] // Antwortmonitore einrichten responseMonitor.didStartLoadingCallback = {vm in XCTAssert (vm.showLoading, "Ladeflag sollte wahr sein.") XCTAssert (! vm.showError, "Fehler Flag sollte nicht wahr sein. ") gestartetLoadingExpectation.fulfill ()} responseMonitor.errorDidOccurCallback = {vm in XCTAssert (! vm.showLoading," Ladeflag sollte nicht wahr sein. ") XCTAssert (vm.showError," Fehlerflag sollte wahr sein . ") XCTAssert (vm.errorMessage ==" Derzeit keine Spiele verfügbar. "," Ungültige Fehlermeldung. ") XCTAssert (vm.itemsCount == 0," Anzahl der Elemente sollte 0 sein. ") ErrorOccuredExpectation.fulfill () } responseMonitor.itemsLoadedCallback = {_ in XCTAssert (false , "Rückruf geladener Elemente sollte nicht aufgerufen werden.")} // Aktion vm.loadGames () ausführen // auf Erwartungswarte prüfen (auf: [StartLoadingExpectation, errorOccuredExpectation], Zeitüberschreitung: 1 ,forceOrder: true)}
Fazit
Wir haben gesehen, wie MVVM es einfach macht, das Benutzerverhalten zu simulieren, ohne sich mit der Ansicht zu befassen. Wir haben drei Testfälle geschrieben, um zu sehen, wie wir verschiedene Zustände der Ansicht testen können.
Sie können das endgültige Projekt hier herunterladen. Auch wenn Sie Fragen oder Anregungen haben, schreiben Sie unten einen Kommentar.