So testen wir Sberbank Online unter iOS



    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 erhalten.

    Die Situation ist wie folgt: Nehmen wir an, 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 derart 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 (Nicht-Integrations-) Unit-Tests auftreten. Im zweiten Teil werden wir uns mit UI-Tests befassen. Willkommen bei Cat, um herauszufinden, wie wir die Stabilität von Testläufen verbessern.

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


    In der Realität kann es vorkommen, dass derselbe Einheitentest entweder ein positives oder ein negatives Ergebnis liefert - was „blinken“ bedeutet. Der Grund für dieses Verhalten liegt in der schlechten Implementierung des Testcodes. Darüber hinaus kann ein solcher Test die CI mit einem erfolgreichen Durchlauf bestehen und später auf die Pull-Anfrage (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 von Tests untergräbt und CI mit bedeutungsloser Arbeit belastet.

    Dieses Problem wurde in diesem Jahr auf der internationalen WWDC-Konferenz von Apple hervorgehoben:

    • Diese Sitzung befasst sich mit dem parallelen Testen, der Analyse der Abdeckung eines einzelnen Zielcodes mit Tests und dem Verfahren zum Ausführen von Tests.
    • Hier sprach Apple über das Testen von Netzwerkanforderungen, das Hacken, das Testen von Benachrichtigungen und die Testgeschwindigkeit.

    Unit-Tests


    Um den Blinktests entgegenzuwirken, verwenden wir die folgende Abfolge von Aktionen:

    Bild

    0. Wir bewerten den Qualitätstestcode anhand der grundlegenden Kriterien: Isolation, Richtigkeit von Mocks usw. Wir folgen der Regel: Mit einem blinkenden Test ändern wir den Testcode und nicht den Testcode.

    Wenn dies nicht hilft, gehen Sie wie folgt vor:

    1. Wir korrigieren und reproduzieren die Bedingungen, unter denen der Test abläuft .
    2. Finden Sie den Grund für den Sturz;
    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 ist.

    Spiel fallen


    Die einfachste und naheliegendste Möglichkeit besteht darin, einen Problemtest auf derselben iOS-Version und demselben Gerät durchzuführen. In diesem Fall ist der Test in der Regel erfolgreich, und es erscheint der Gedanke: „Bei mir funktioniert alles vor Ort, ich starte die Assembly auf CI neu.“ Tatsächlich ist das Problem jedoch noch nicht gelöst, und der Test wird weiterhin von einer anderen Person durchgeführt.

    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 das Testergebnis positiv sein, das Problem bleibt jedoch unentdeckt.

    Wenn die gesamte Testsequenz erfolgreich war und der erwartete Abfall nicht behoben werden konnte, können Sie den Lauf mehrmals 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 für den aufgezeichneten Sturz.

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


    Betrachten Sie die Hauptursachen für blinkende Komponententests, auf die Sie bei Ihrer Arbeit stoßen können, Tools, um sie zu identifizieren, und mögliche Lösungen.

    Wir können drei Hauptgruppen von Gründen für den Sturz der Tests unterscheiden:

    Schwache Isolation

    Unter Isolation versteht man einen Sonderfall 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 die getesteten Einheiten nicht beeinträchtigt werden dürfen. Besondere Aufmerksamkeit sollte Tests gewidmet werden, die auf die Überprüfung des Codes abzielen. Sie verwenden globale Zustandsentitäten, z. B. globale Variablen, Schlüsselbund, Netzwerk, CoreData, Singleton, NSUserDefaults usw. In diesen Gebieten gibt es 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 prüft, möglicherweise zu „blinken“, da je nach Testreihenfolge zwei Situationen auftreten können: Wenn der globale Status festgelegt ist und wenn er nicht festgelegt ist. Oft sind die beschriebenen Abhängigkeiten implizit.

    Damit die Abhängigkeiten klar erkennbar sind, können Sie das Prinzip der Abhängigkeitsinjektion (Dependency Injection, DI) verwenden: Übergeben Sie die Abhängigkeit über die Parameter des Konstruktors oder eine Eigenschaft des Objekts. Dies erleichtert das Ersetzen von Scheinabhängigkeiten anstelle eines realen Objekts.

    Asynchrone Anrufe

    Alle Unit-Tests werden synchron durchgeführt. Die Schwierigkeit beim Testen der Asynchronität entsteht, weil der Aufruf der Testmethode im Test im Vorgriff auf den Abschluss des Unit-Test-Gültigkeitsbereichs "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 ausreicht. Vor Ort besteht Ihr Test zwar, aber auf einem stark belasteten CI ist möglicherweise nicht genügend Strom verfügbar, und der Test fällt ab. Ab diesem Zeitpunkt wird ein "Blinken" angezeigt.

    Lassen Sie uns eine Art Datenverarbeitungsservice haben. Wir möchten sicherstellen, dass nach Erhalt einer Antwort vom Server diese Daten 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 unter Verwendung eines Scheinservers asynchron geschrieben werden, um stabile Netzwerkantworten zu gewährleisten.

    
    @interface Service : NSObject
    @property (nonatomic, strong) id apiClient;
    @end
    @protocol APIClient 
    - (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, das Arbeiten mit Zeitüberschreitungen loszuwerden.

    Für ihn brauchen wir einen synchronen Mock-APIClient

    
    @interface APIClientMock : NSObject 
    @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 eingekapselt 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 Benutzeroberflächenebene über den Hintergrundthread überprüfen, ob wir uns im Hauptthread befinden und was passiert, wenn wir einen Testaufruf ausführen:

    
    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 in dem Artikel von D. Sandell .

    Testen von Code, der sich Ihrer Kontrolle
    entzieht Wir vergessen häufig die folgenden Dinge:

    • 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 hängt möglicherweise von der Version des SDK ab


    Die oben genannten Fälle führen zu Unsicherheiten beim Schreiben und Ausführen von Tests. Um negative Konsequenzen zu vermeiden, müssen Sie Tests in allen Ländereinstellungen sowie in Versionen von iOS ausführen, die von Ihrer Anwendung unterstützt werden. Unabhängig davon ist zu beachten, 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 zum Testen von Einheiten vervollständigen.

    Im zweiten Teil des Artikels werden wir auf die Probleme eingehen, die beim Schreiben von 1500 UI-Tests aufgetreten sind, sowie auf die Rezepte für deren Überwindung.

    Der Artikel wurde von Regno - Anton Vlasov, Entwicklungsleiter und iOS-Entwickler, verfasst.

    Jetzt auch beliebt: