Wie wir Sberbank Online unter iOS testen



Im vorherigen Artikel haben wir uns mit der Testpyramide und den Vorteilen automatisierter Tests vertraut gemacht. Aber die Theorie unterscheidet sich normalerweise von der Praxis. Heute möchten wir über unsere Erfahrungen beim Testen von Anwendungscode sprechen, der von Millionen von iOS-Benutzern verwendet wird. Und auch über den schwierigen Weg, den unser Team gehen musste, um stabilen Code zu erreichen.

Die Situation ist folgende: Angenommen, die Entwickler haben es geschafft, sich und das Unternehmen von der Notwendigkeit zu überzeugen, die Codebasis mit Tests abzudecken. Im Laufe der Zeit hat sich das Projekt zu mehr als einem Dutzend Tausend Unit- und mehr als Tausend UI-Tests entwickelt. Eine so große Testbasis führte zu mehreren Problemen, deren Lösung wir erläutern möchten.

Im ersten Teil des Artikels werden wir uns mit den Schwierigkeiten vertraut machen, die bei der Arbeit mit sauberen Unit-Tests (ohne Integration) auftreten. Im zweiten Teil werden wir UI-Tests betrachten. Willkommen bei Cat, um herauszufinden, wie wir die Stabilität von Testläufen verbessern.

In einer idealen Welt mit unverändertem Quellcode sollten Unit-Tests unabhängig von Anzahl und Reihenfolge der Starts immer das gleiche Ergebnis zeigen. Ständig fallende Tests sollten die CI-Barriere (Continuous Integration Server) nicht passieren.


In der Realität kann man auf die Tatsache stoßen, dass der gleiche Komponententest entweder ein positives oder ein negatives Ergebnis zeigt - was „blinken“ bedeutet. Der Grund für dieses Verhalten liegt in der schlechten Implementierung des Testcodes. Darüber hinaus kann ein solcher Test CI mit einem erfolgreichen Lauf bestehen und später auf die Pull Request (PR) anderer Personen fallen. In einer ähnlichen Situation besteht der Wunsch, diesen Test zu deaktivieren oder Roulette zu spielen und den CI-Lauf erneut auszuführen. Dieser Ansatz ist jedoch nicht produktiv, da er die Glaubwürdigkeit der Tests untergräbt und CI mit bedeutungsloser Arbeit belastet.

Dieses Problem wurde dieses Jahr auf der internationalen WWDC-Konferenz von Apple vorgestellt:

  • In dieser Sitzung geht es um parallele Tests, die Analyse der Abdeckung eines einzelnen Zielcodes mit Tests sowie um die Reihenfolge des Startens von Tests.
  • Hier sprach Apple über das Testen von Netzwerkanforderungen, Hacken, Testen von Benachrichtigungen und die Geschwindigkeit von Tests.

Unit-Tests


Um Blinktests zu bekämpfen, verwenden wir die folgende Abfolge von Aktionen:

Bild

0. Wir bewerten den Qualitätstestcode nach grundlegenden Kriterien: Isolierung, Korrektheit von Moka usw. Wir folgen der Regel: Bei einem blinkenden Test ändern wir den Testcode und nicht den Testcode.

Wenn dieser Punkt nicht hilft, gehen Sie wie folgt vor:

1. Wir korrigieren und reproduzieren die Bedingungen, unter denen der Test fällt.
2. Finden Sie den Grund, warum der Fall;
3. Ändern Sie den Testcode oder den Testcode.
4. Fahren Sie mit dem ersten Schritt fort und prüfen Sie, ob die Ursache des Sturzes behoben wurde.

Spielen Sie Herbst


Die einfachste und naheliegendste Option besteht darin, einen Problemtest auf derselben iOS-Version und auf demselben Gerät durchzuführen. In diesem Fall ist der Test in der Regel erfolgreich und der Gedanke erscheint: „Alles funktioniert für mich lokal, ich werde die Assembly auf CI neu starten.“ Das ist nur in der Tat das Problem wurde nicht gelöst, und der Test fällt weiterhin mit jemand anderem.

Daher müssen Sie im nächsten Überprüfungsschritt alle Komponententests der Anwendung lokal ausführen, um die möglichen Auswirkungen eines Tests auf einen anderen zu ermitteln. Aber auch nach einer solchen Überprüfung kann Ihr Testergebnis positiv sein, aber das Problem bleibt unentdeckt.

Wenn die gesamte Testsequenz erfolgreich war und der erwartete Abfall nicht aufgezeichnet wurde, können Sie den Lauf erheblich wiederholen.
Dazu müssen Sie in der Befehlszeile eine Schleife mit xcodebuild ausführen:

#! /bin/sh x=0 while [ $x -le 100 ]; do xcodebuild -configuration Debug -scheme "TargetScheme" -workspace App.wcworkspace -sdk iphonesimulator -destination "platfrom=iOS Simulator, OS=11.3, name=iPhone 7" test >> "report.txt"; x=$(( $x +1 )); done 

In der Regel reicht dies aus, um den Sturz zu reproduzieren und mit dem nächsten Schritt fortzufahren - der Ermittlung der Ursache des aufgezeichneten Sturzes.

Gründe für den Herbst und mögliche Lösungen


Berücksichtigen Sie die Hauptursachen für blinkende Komponententests, die bei Ihrer Arbeit auftreten können, Tools zur Identifizierung und mögliche Lösungen.

Es gibt drei Hauptgruppen von Gründen für den Fall von Tests:

Schlechte Isolierung

Mit Isolation meinen wir einen speziellen Fall der Kapselung, nämlich: einen Sprachmechanismus, der es ermöglicht, den Zugriff einiger Programmkomponenten auf andere zu beschränken.

Die Isolierung der Umgebung spielt eine wichtige Rolle, da für die Reinheit des Tests nichts die getesteten Entitäten beeinflussen sollte. Besondere Aufmerksamkeit sollte Tests gewidmet werden, die auf die Überprüfung des Codes abzielen. Sie verwenden globale Statusentitäten wie globale Variablen, Schlüsselbund, Netzwerk, CoreData, Singleton, NSUserDefaults usw. In diesen Gebieten entstehen die meisten potenziellen Orte für die Manifestation einer schlechten Isolation. Angenommen, beim Erstellen einer Testumgebung wird ein globaler Status festgelegt, der implizit in einem anderen Testcode verwendet wird. In diesem Fall beginnt der Test, der den zu testenden Code überprüft, möglicherweise zu „blinken“, da abhängig von der Testsequenz zwei Situationen auftreten können - wenn der globale Status festgelegt ist und wenn er nicht festgelegt ist. Oft sind die beschriebenen Abhängigkeiten implizit, sodass Sie möglicherweise versehentlich vergessen, solche globalen Zustände festzulegen / zurückzusetzen.

Um die Abhängigkeiten deutlich sichtbar zu machen, können Sie das DI-Prinzip (Dependency Injection) verwenden: Übergeben Sie die Abhängigkeit über die Konstruktorparameter oder die Eigenschaft des Objekts. Dies macht es einfach, Scheinabhängigkeiten anstelle eines realen Objekts zu ersetzen.

Asynchronität aufrufen

Alle Unit-Tests werden synchron durchgeführt. Die Schwierigkeit, die Asynchronität zu testen, entsteht, weil der Aufruf der Testmethode im Test in Erwartung des Abschlusses des Komponententests „einfriert“. Das Ergebnis ist ein stabiler Abfall im Test.

 //act [self.testService loadImageFromUrl:@"www.google.ru" handler:^(UIImage * _Nullable image, NSError * _Nullable error) { //assert OCMVerify([cacheMock imageAtPath:OCMOCK_ANY]); OCMVerify([cacheMock dateOfFileAtPath:OCMOCK_ANY]); OCMVerify([imageMock new]); [imageMock stopMocking]; }]; [self waitInterval:0.2]; 

Um einen solchen Test zu testen, gibt es verschiedene Ansätze:

  1. Führen Sie NSRunLoop aus
  2. waitForExpectationsWithTimeout

Bei beiden Optionen müssen Sie ein Argument mit einer Zeitüberschreitung angeben. Es kann jedoch nicht garantiert werden, dass das ausgewählte Intervall ausreichend ist. Vor Ort besteht Ihr Test, aber auf einem stark belasteten CI ist möglicherweise nicht genügend Strom vorhanden, und er fällt ab - daher das „Blinken“.

Lassen Sie uns eine Art Datenverarbeitungsdienst haben. Wir möchten überprüfen, ob diese Daten nach Erhalt einer Antwort vom Server zur weiteren Verarbeitung übertragen werden.

Um Anforderungen über das Netzwerk zu senden, verwendet der Dienst den Client, um damit zu arbeiten.

Ein solcher Test kann asynchron unter Verwendung eines Mock-Servers geschrieben werden, um stabile Netzwerkantworten zu gewährleisten.

 @interface Service : NSObject @property (nonatomic, strong) id<APIClient> apiClient; @end @protocol APIClient <NSObject> - (void)getDataWithCompletion:(void (^)(id responseJSONData))completion; @end - (void)testRequestAsync { // arrange __auto_type service = [Service new]; service.apiClient = [APIClient new]; XCTestExpectation *expectation = [self expectationWithDescription:@"Request"]; // act id receivedData = nil; [self.service receiveDataWithCompletion:^(id responseJSONData) { receivedData = responseJSONData; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { expect(receivedData).notTo.beNil(); expect(error).to.beNil(); }]; } 

Die synchrone Version des Tests ist jedoch stabiler und ermöglicht es Ihnen, die Arbeit mit Zeitüberschreitungen zu vermeiden.

Für ihn brauchen wir einen synchronen Schein-APIClient

 @interface APIClientMock : NSObject <APIClient> @end @implementation - (void)getDataWithCompletion:(void (^)(id responseJSONData))completion { __auto_type fakeData = @{ @"key" : @"value" }; if (completion != nil) { completion(fakeData); } } @end 

Dann sieht der Test einfacher aus und arbeitet stabiler

 - (void)testRequestSync { // arrange __auto_type service = [Service new]; service.apiClient = [APIClientMock new]; // act id receivedData = nil; [self.service receiveDataWithCompletion:^(id responseJSONData) { receivedData = responseJSONData; }]; expect(receivedData).notTo.beNil(); expect(error).to.beNil(); } 

Der asynchrone Betrieb kann isoliert werden, indem eine separate Entität gekapselt wird, die unabhängig getestet werden kann. Der Rest der Logik muss synchron getestet werden. Dieser Ansatz vermeidet die meisten Fallstricke, die durch Asynchronität entstehen.

Optional können Sie beim Aktualisieren der UI-Ebene aus dem Hintergrund-Thread überprüfen, ob wir uns im Haupt-Thread befinden und was passiert, wenn wir vom Test aus einen Aufruf tätigen:

 func performUIUpdate(using closure: @escaping () -> Void) { // If we are already on the main thread, execute the closure directly if Thread.isMainThread { closure() } else { DispatchQueue.main.async(execute: closure) } } 

Eine ausführliche Erklärung finden Sie im Artikel von D. Sandell .

Testen von Code außerhalb Ihrer Kontrolle
Oft vergessen wir Folgendes:

  • Die Implementierung der Methoden kann von der Lokalisierung der Anwendung abhängen.
  • Es gibt private Methoden im SDK, die von Framework-Klassen aufgerufen werden können.
  • Die Implementierung der Methoden kann von der Version des SDK abhängen


Die oben genannten Fälle führen zu Unsicherheit beim Schreiben und Ausführen von Tests. Um negative Konsequenzen zu vermeiden, müssen Sie Tests für alle Gebietsschemas sowie für Versionen von iOS ausführen, die von Ihrer Anwendung unterstützt werden. Unabhängig davon sollte beachtet werden, dass kein Code getestet werden muss, dessen Implementierung vor Ihnen verborgen ist.

Damit möchten wir den ersten Teil des Artikels über das automatisierte Testen der Sberbank Online iOS-Anwendung abschließen, der dem Testen von Einheiten gewidmet ist.

Im zweiten Teil des Artikels werden wir über die Probleme sprechen, die beim Schreiben von 1500 UI-Tests aufgetreten sind, sowie über Rezepte zu deren Überwindung.

Der Artikel wurde mit regno - Anton Vlasov, Entwicklungsleiter und iOS-Entwickler, geschrieben.

Source: https://habr.com/ru/post/de434320/


All Articles