iOS Unit Testing und UI Testing Tutorial

Aktualisierungshinweis: Michael Katz hat dieses Tutorial für Xcode 10.1, Swift 4.2 und iOS 12 aktualisiert. Audrey Tam hat das Original verfasst.

Tests zu schreiben ist nicht glamourös, aber da Tests verhindern, dass sich Ihre glitzernde App in ein fehlerbehaftetes Stück Schrott verwandelt, sind sie notwendig. Wenn Sie dieses Tutorial lesen, wissen Sie bereits, dass Sie Tests für Ihren Code und Ihre Benutzeroberfläche schreiben sollten, aber Sie wissen vielleicht nicht, wie.

Sie haben vielleicht eine funktionierende Anwendung, aber Sie möchten Änderungen testen, die Sie vornehmen, um die Anwendung zu erweitern. Vielleicht haben Sie bereits Tests geschrieben, sind sich aber nicht sicher, ob es die richtigen Tests sind. Oder Sie haben mit der Arbeit an einer neuen Anwendung begonnen und möchten sie nach und nach testen.

Dieses Tutorial wird Ihnen das zeigen:

  • Wie Sie den Testnavigator von Xcode verwenden, um das Modell und die asynchronen Methoden einer App zu testen
  • Wie Sie Interaktionen mit Bibliotheks- oder Systemobjekten mithilfe von Stubs und Mocks vortäuschen
  • Wie Sie die Benutzeroberfläche und die Leistung testen
  • Wie Sie das Code Coverage Tool verwenden

Auf dem Weg dahin, lernen Sie einen Teil des Vokabulars der Test-Ninjas kennen.

Herausfinden, was getestet werden soll

Bevor man Tests schreibt, ist es wichtig, die Grundlagen zu kennen. Was müssen Sie testen?

Wenn Sie eine bestehende Anwendung erweitern wollen, sollten Sie zunächst Tests für jede Komponente schreiben, die Sie ändern wollen.

Generell sollten die Tests Folgendes abdecken:

  • Kernfunktionalität: Modellklassen und -methoden und ihre Interaktionen mit dem Controller
  • Die häufigsten UI-Workflows
  • Randbedingungen
  • Bugfixes

Best Practices for Testing

Das Akronym FIRST beschreibt einen prägnanten Satz von Kriterien für effektive Unit-Tests. Diese Kriterien sind:

  • Schnell: Tests sollten schnell ausgeführt werden.
  • Unabhängig/Isoliert: Die Tests sollten keinen gemeinsamen Status haben.
  • Wiederholbar: Sie sollten jedes Mal, wenn Sie einen Test ausführen, die gleichen Ergebnisse erhalten. Externe Datenlieferanten oder Gleichzeitigkeitsprobleme könnten zu zeitweiligen Fehlern führen.
  • Selbstvalidierend: Die Tests sollten vollständig automatisiert sein. Die Ausgabe sollte entweder „bestanden“ oder „fehlgeschlagen“ sein, anstatt sich auf die Interpretation einer Protokolldatei durch einen Programmierer zu verlassen.
  • Rechtzeitig: Idealerweise sollten Tests geschrieben werden, bevor Sie den Produktionscode schreiben, den sie testen (testgetriebene Entwicklung).

Wenn Sie die FIRST-Prinzipien befolgen, werden Ihre Tests klar und hilfreich sein, anstatt sich in Straßensperren für Ihre Anwendung zu verwandeln.

Anfangen

Beginnen Sie, indem Sie die Projektmaterialien über die Schaltfläche „Materialien herunterladen“ oben oder unten in diesem Tutorial herunterladen. Es gibt zwei separate Startprojekte: BullsEye und HalfTunes.

  • BullsEye basiert auf einer Beispiel-App in iOS Apprentice. Die Spiellogik befindet sich in der Klasse BullsEyeGame, die Sie in diesem Tutorial testen werden.
  • HalfTunes ist eine aktualisierte Version der Beispiel-App aus dem URLSession Tutorial. Benutzer können die iTunes-API nach Liedern abfragen und dann Liedausschnitte herunterladen und abspielen.

Unit-Tests in Xcode

Der Testnavigator bietet die einfachste Möglichkeit, mit Tests zu arbeiten; Sie werden ihn verwenden, um Testziele zu erstellen und Tests für Ihre Anwendung auszuführen.

Erstellen eines Unit-Test-Targets

Öffnen Sie das BullsEye-Projekt und drücken Sie Command-6, um den Test-Navigator zu öffnen.

Klicken Sie auf die +-Schaltfläche in der linken unteren Ecke und wählen Sie dann New Unit Test Target… aus dem Menü aus:

Übernehmen Sie den Standardnamen, BullsEyeTests. Wenn das Testbündel im Testnavigator angezeigt wird, klicken Sie darauf, um das Bündel im Editor zu öffnen. Wenn das Bundle nicht automatisch angezeigt wird, klicken Sie auf einen der anderen Navigatoren und kehren Sie dann zum Test-Navigator zurück.

Die Standardvorlage importiert das Testframework XCTest und definiert eine BullsEyeTests Unterklasse von XCTestCase mit setUp(), tearDown() und Beispieltestmethoden.

Es gibt drei Möglichkeiten, die Tests auszuführen:

  1. Produkt ▸ Test oder Befehl-U. Beide führen alle Testklassen aus.
  2. Klicken Sie auf die Pfeilschaltfläche im Testnavigator.
  3. Klicken Sie auf die Rautenschaltfläche in der Rinne.

Sie können auch eine einzelne Testmethode ausführen, indem Sie auf die entsprechende Raute klicken, entweder im Testnavigator oder in der Gosse.

Proben Sie die verschiedenen Möglichkeiten, Tests auszuführen, um ein Gefühl dafür zu bekommen, wie lange es dauert und wie es aussieht. Die Beispieltests führen noch nichts aus, sie laufen also sehr schnell!

Wenn alle Tests erfolgreich sind, werden die Rauten grün und zeigen Häkchen. Sie können auf die graue Raute am Ende von testPerformanceExample() klicken, um das Leistungsergebnis zu öffnen:

Sie brauchen testPerformanceExample() oder testExample() für dieses Tutorial nicht, also löschen Sie sie.

Verwendung von XCTAssert zum Testen von Modellen

Zunächst werden Sie XCTAssert Funktionen verwenden, um eine Kernfunktion des BullsEye-Modells zu testen: Berechnet ein BullsEyeGame Objekt korrekt den Punktestand für eine Runde?

Fügen Sie in BullsEyeTests.swift diese Zeile direkt unter der import Anweisung ein:

@testable import BullsEye

Damit erhalten die Unit-Tests Zugriff auf die internen Typen und Funktionen von BullsEye.

Fügen Sie am Anfang der Klasse BullsEyeTests diese Eigenschaft hinzu:

var sut: BullsEyeGame!

Dies erzeugt einen Platzhalter für ein BullsEyeGame, das das zu testende System (SUT) ist, oder das Objekt, das diese Testfallklasse testen soll.

Als Nächstes ersetzen Sie den Inhalt von setup() wie folgt:

super.setUp()sut = BullsEyeGame()sut.startNewGame()

Dadurch wird ein BullsEyeGame-Objekt auf Klassenebene erstellt, so dass alle Tests in dieser Testklasse auf die Eigenschaften und Methoden des SUT-Objekts zugreifen können.

Hier rufen Sie auch das startNewGame() des Spiels auf, das das targetValue initialisiert. Viele der Tests werden targetValue verwenden, um zu testen, dass das Spiel den Spielstand korrekt berechnet.

Bevor Sie es vergessen, geben Sie Ihr SUT-Objekt in tearDown() frei. Ersetzen Sie seinen Inhalt durch:

sut = nilsuper.tearDown()
Hinweis: Es ist eine gute Praxis, das SUT in setUp() zu erstellen und es in tearDown() freizugeben, um sicherzustellen, dass jeder Test mit einer sauberen Weste beginnt. Weitere Informationen finden Sie in Jon Reids Beitrag zu diesem Thema.

Den ersten Test schreiben

Jetzt sind Sie bereit, Ihren ersten Test zu schreiben!

Fügen Sie den folgenden Code am Ende von BullsEyeTests ein:

func testScoreIsComputed() { // 1. given let guess = sut.targetValue + 5 // 2. when sut.check(guess: guess) // 3. then XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")}

Der Name einer Testmethode beginnt immer mit test, gefolgt von einer Beschreibung dessen, was sie testet.

Es ist eine gute Praxis, den Test in given, when und dann in Abschnitte zu unterteilen:

  1. Given: Hier legen Sie alle benötigten Werte fest. In diesem Beispiel erstellen Sie einen guess-Wert, damit Sie angeben können, wie sehr er sich von targetValue unterscheidet.
  2. When: In diesem Abschnitt führen Sie den zu testenden Code aus: Aufruf check(guess:).
  3. Dann: In diesem Abschnitt geben Sie das erwartete Ergebnis mit einer Meldung an, die gedruckt wird, wenn der Test fehlschlägt. In diesem Fall sollte sut.scoreRound gleich 95 (100 – 5) sein.

Starten Sie den Test, indem Sie auf das Rautensymbol in der Gosse oder im Testnavigator klicken. Dadurch wird die Anwendung erstellt und ausgeführt, und das Rautensymbol ändert sich in ein grünes Häkchen!

Hinweis: Eine vollständige Liste der XCTestAssertions finden Sie unter Apple’s Assertions Listed by Category.

Debugging a Test

Es gibt einen Fehler, der absichtlich in BullsEyeGame eingebaut wurde, und Sie werden jetzt üben, ihn zu finden. Um den Fehler in Aktion zu sehen, erstellen Sie einen Test, der im angegebenen Abschnitt 5 von targetValue subtrahiert und alles andere unverändert lässt.

Fügen Sie den folgenden Test hinzu:

func testScoreIsComputedWhenGuessLTTarget() { // 1. given let guess = sut.targetValue - 5 // 2. when sut.check(guess: guess) // 3. then XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")}

Die Differenz zwischen guess und targetValue ist immer noch 5, also sollte die Punktzahl immer noch 95 sein.

Fügen Sie im Haltepunktnavigator einen Haltepunkt für Testfehler hinzu. Dadurch wird der Testlauf angehalten, wenn eine Testmethode eine Fehlermeldung abgibt.

Führen Sie Ihren Test aus, und er sollte bei der Zeile XCTAssertEqual mit einem Testfehler anhalten.

Untersuchen Sie sut und guess in der Debug-Konsole:

guess ist targetValue - 5, aber scoreRound ist 105, nicht 95!

Um den Test weiter zu untersuchen, verwenden Sie den normalen Debugging-Prozess: Setzen Sie einen Haltepunkt bei der when-Anweisung und auch einen in BullsEyeGame.swift, innerhalb von check(guess:), wo difference erzeugt wird. Führen Sie dann den Test erneut aus und gehen Sie über die let difference-Anweisung, um den Wert von difference in der Anwendung zu untersuchen:

Das Problem ist, dass difference negativ ist, also ist der Wert 100 – (-5). Um dies zu beheben, sollten Sie den absoluten Wert von difference verwenden. Entfernen Sie in check(guess:) die richtige Zeile und löschen Sie die falsche.

Entfernen Sie die beiden Haltepunkte und führen Sie den Test erneut aus, um zu bestätigen, dass er jetzt erfolgreich ist.

Verwenden von XCTestExpectation zum Testen asynchroner Operationen

Nachdem Sie nun gelernt haben, wie man Modelle testet und Testfehler debuggt, ist es an der Zeit, zum Testen von asynchronem Code überzugehen.

Öffnen Sie das HalfTunes-Projekt. Es verwendet URLSession, um die iTunes-API abzufragen und Songbeispiele herunterzuladen. Nehmen wir an, Sie möchten es so ändern, dass es AlamoFire für Netzwerkoperationen verwendet. Um zu sehen, ob etwas nicht funktioniert, sollten Sie Tests für die Netzwerkoperationen schreiben und sie vor und nach der Änderung des Codes ausführen.

URLSessionMethoden sind asynchron: Sie kehren sofort zurück, beenden die Ausführung aber erst später. Um asynchrone Methoden zu testen, verwenden Sie XCTestExpectation, um Ihren Test auf den Abschluss der asynchronen Operation warten zu lassen.

Asynchrone Tests sind in der Regel langsam, daher sollten Sie sie von Ihren schnelleren Unit-Tests getrennt halten.

Erstellen Sie ein neues Unit-Test-Ziel mit dem Namen HalfTunesSlowTests. Öffnen Sie die Klasse HalfTunesSlowTests und importieren Sie das HalfTunes-App-Modul direkt unter der bestehenden import-Anweisung:

@testable import HalfTunes

Alle Tests in dieser Klasse verwenden den Standard URLSession, um Anfragen an die Apple-Server zu senden, deklarieren Sie also ein sut-Objekt, erstellen Sie es in setUp() und geben Sie es in tearDown() frei.

Ersetzen Sie den Inhalt der Klasse HalfTunesSlowTests durch:

var sut: URLSession!override func setUp() { super.setUp() sut = URLSession(configuration: .default)}override func tearDown() { sut = nil super.tearDown()}

Fügen Sie als Nächstes diesen asynchronen Test hinzu:

// Asynchronous test: success fast, failure slowfunc testValidCallToiTunesGetsHTTPStatusCode200() { // given let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") // 1 let promise = expectation(description: "Status code: 200") // when let dataTask = sut.dataTask(with: url!) { data, response, error in // then if let error = error { XCTFail("Error: \(error.localizedDescription)") return } else if let statusCode = (response as? HTTPURLResponse)?.statusCode { if statusCode == 200 { // 2 promise.fulfill() } else { XCTFail("Status code: \(statusCode)") } } } dataTask.resume() // 3 wait(for: , timeout: 5)}

Dieser Test prüft, ob das Senden einer gültigen Anfrage an iTunes einen Statuscode 200 zurückgibt. Der größte Teil des Codes ist derselbe, den Sie in der App schreiben würden, mit diesen zusätzlichen Zeilen:

  1. expectation(description:): Gibt ein XCTestExpectation Objekt zurück, das in promise gespeichert ist. Der Parameter description beschreibt, was Sie erwarten.
  2. promise.fulfill(): Rufen Sie dies in der Erfolgsbedingungsschließung des Abschlusshandlers der asynchronen Methode auf, um zu kennzeichnen, dass die Erwartung erfüllt wurde.
  3. wait(for:timeout:): Lässt den Test weiterlaufen, bis alle Erwartungen erfüllt sind oder das timeout Intervall endet, je nachdem, was zuerst eintritt.

Starten Sie den Test. Wenn Sie mit dem Internet verbunden sind, sollte es etwa eine Sekunde dauern, bis der Test erfolgreich ist, nachdem die App im Simulator geladen wurde.

Schnell scheitern

Ein Fehlschlag tut weh, aber es muss nicht ewig dauern.

Um einen Fehlschlag zu erleben, löschen Sie einfach das ’s‘ aus „itunes“ in der URL:

let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")

Starten Sie den Test. Der Test schlägt fehl, aber er dauert das gesamte Timeout-Intervall! Das liegt daran, dass Sie davon ausgingen, dass die Anfrage immer erfolgreich sein würde, und daher promise.fulfill() aufriefen. Da die Anfrage fehlschlug, wurde sie erst beendet, als die Zeitüberschreitung abgelaufen war.

Sie können dies verbessern und den Test schneller fehlschlagen lassen, indem Sie die Annahme ändern: Anstatt auf den Erfolg der Anfrage zu warten, warten Sie nur, bis der Completion-Handler der asynchronen Methode aufgerufen wird. Dies geschieht, sobald die Anwendung eine Antwort – entweder OK oder Fehler – vom Server erhält, die die Erwartung erfüllt. Ihr Test kann dann überprüfen, ob die Anfrage erfolgreich war.

Um zu sehen, wie das funktioniert, erstellen Sie einen neuen Test.

Bessern Sie jedoch zunächst den vorherigen Test, indem Sie die Änderung an url rückgängig machen.
Fügen Sie dann den folgenden Test zu Ihrer Klasse hinzu:

func testCallToiTunesCompletes() { // given let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba") let promise = expectation(description: "Completion handler invoked") var statusCode: Int? var responseError: Error? // when let dataTask = sut.dataTask(with: url!) { data, response, error in statusCode = (response as? HTTPURLResponse)?.statusCode responseError = error promise.fulfill() } dataTask.resume() wait(for: , timeout: 5) // then XCTAssertNil(responseError) XCTAssertEqual(statusCode, 200)}

Der Hauptunterschied besteht darin, dass die Erwartung bereits durch die Eingabe des Completion-Handlers erfüllt wird, und dies dauert nur etwa eine Sekunde. Wenn die Anfrage fehlschlägt, schlagen die thenAssertions fehl.

Führen Sie den Test aus. Es sollte jetzt etwa eine Sekunde dauern, bis er fehlschlägt. Er schlägt fehl, weil die Anfrage fehlgeschlagen ist, nicht weil der Testlauf timeout überschritten hat.

Beheben Sie die url und führen Sie den Test erneut aus, um zu bestätigen, dass er jetzt erfolgreich ist.

Fälschen von Objekten und Interaktionen

Asynchrone Tests geben Ihnen die Gewissheit, dass Ihr Code korrekte Eingaben für eine asynchrone API erzeugt. Sie möchten vielleicht auch testen, dass Ihr Code korrekt funktioniert, wenn er Eingaben von einem URLSession empfängt, oder dass er die Standarddatenbank des Benutzers oder einen iCloud-Container korrekt aktualisiert.

Die meisten Anwendungen interagieren mit System- oder Bibliotheksobjekten – Objekten, die Sie nicht kontrollieren – und Tests, die mit diesen Objekten interagieren, können langsam und unwiederholbar sein, was gegen zwei der FIRST-Prinzipien verstößt. Stattdessen können Sie die Interaktionen vortäuschen, indem Sie Eingaben von Stubs erhalten oder Mock-Objekte aktualisieren.

Wenden Sie die Vortäuschung an, wenn Ihr Code eine Abhängigkeit von einem System- oder Bibliotheksobjekt hat. Sie können dies tun, indem Sie ein gefälschtes Objekt erstellen, das diese Rolle spielt, und dieses gefälschte Objekt in Ihren Code injizieren. Dependency Injection von Jon Reid beschreibt mehrere Möglichkeiten, dies zu tun.

Fake Input From Stub

In diesem Test überprüfen Sie, ob die App updateSearchResults(_:) die von der Sitzung heruntergeladenen Daten korrekt parst, indem Sie prüfen, ob searchResults.count korrekt ist. Das SUT ist der View Controller, und Sie werden die Sitzung mit Stubs und einigen zuvor heruntergeladenen Daten vortäuschen.

Gehen Sie zum Testnavigator und fügen Sie ein neues Unit Test Target hinzu. Nennen Sie es HalfTunesFakeTests. Öffnen Sie HalfTunesFakeTests.swift und importieren Sie das HalfTunes-App-Modul direkt unter der import-Anweisung:

@testable import HalfTunes

Ersetzen Sie nun den Inhalt der Klasse HalfTunesFakeTests durch Folgendes:

var sut: SearchViewController!override func setUp() { super.setUp() sut = UIStoryboard(name: "Main", bundle: nil) .instantiateInitialViewController() as? SearchViewController}override func tearDown() { sut = nil super.tearDown()}

Dies deklariert das SUT, das ein SearchViewController ist, erstellt es in setUp() und gibt es in tearDown() frei:

Hinweis: Das SUT ist der Viewcontroller, denn HalfTunes hat ein massives Viewcontroller-Problem – die ganze Arbeit wird im SearchViewController erledigt.swift. Das Verschieben des Netzwerkcodes in ein separates Modul würde dieses Problem verringern und außerdem das Testen vereinfachen.

Als Nächstes benötigen Sie einige JSON-Beispieldaten, die Ihre gefälschte Sitzung für Ihren Test bereitstellen wird. Um die Download-Ergebnisse in iTunes zu begrenzen, fügen Sie &limit=3 an die URL-Zeichenfolge an:

https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

Kopieren Sie diese URL und fügen Sie sie in einen Browser ein. Dadurch wird eine Datei mit dem Namen 1.txt, 1.txt.js oder ähnlich geladen. Überprüfen Sie in der Vorschau, ob es sich um eine JSON-Datei handelt, und benennen Sie sie dann in abbaData.json um.

Gehen Sie nun zurück zu Xcode und rufen Sie den Projektnavigator auf. Fügen Sie die Datei zur Gruppe HalfTunesFakeTests hinzu.

Das HalfTunes-Projekt enthält die unterstützende Datei DHURLSessionMock.swift. Diese definiert ein einfaches Protokoll mit dem Namen DHURLSession, mit Methoden (Stubs), um eine Datenaufgabe entweder mit einem URL oder einem URLRequest zu erstellen. Sie definiert auch URLSessionMock, das diesem Protokoll mit Initialisierern entspricht, mit denen Sie ein URLSession Mock-Objekt mit Daten, Antworten und Fehlern Ihrer Wahl erstellen können.

Um den Fake einzurichten, gehen Sie zu HalfTunesFakeTests.swift und fügen Sie das Folgende in setUp() nach der Anweisung hinzu, die das SUT erstellt:

let testBundle = Bundle(for: type(of: self))let path = testBundle.path(forResource: "abbaData", ofType: "json")let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")let urlResponse = HTTPURLResponse( url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)sut.defaultSession = sessionMock

Damit werden die gefälschten Daten und Antworten eingerichtet und das gefälschte Sitzungsobjekt erstellt. Am Ende wird die gefälschte Sitzung als Eigenschaft von sut in die App injiziert.

Jetzt können Sie den Test schreiben, der überprüft, ob der Aufruf von updateSearchResults(_:) die gefälschten Daten analysiert. Fügen Sie den folgenden Test hinzu:

func test_UpdateSearchResults_ParsesData() { // given let promise = expectation(description: "Status code: 200") // when XCTAssertEqual( sut.searchResults.count, 0, "searchResults should be empty before the data task runs") let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") let dataTask = sut.defaultSession.dataTask(with: url!) { data, response, error in // if HTTP request is successful, call updateSearchResults(_:) // which parses the response data into Tracks if let error = error { print(error.localizedDescription) } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { self.sut.updateSearchResults(data) } promise.fulfill() } dataTask.resume() wait(for: , timeout: 5) // then XCTAssertEqual(sut.searchResults.count, 3, "Didn't parse 3 items from fake response")}

Sie müssen diesen Test immer noch als asynchronen Test schreiben, da der Stub vorgibt, eine asynchrone Methode zu sein.

Die wenn-Aussage ist, dass searchResults leer ist, bevor die Datenaufgabe ausgeführt wird. Das sollte wahr sein, weil Sie ein komplett neues SUT in setUp() erstellt haben.

Die gefälschten Daten enthalten das JSON für drei Track Objekte, also ist die then Behauptung, dass das searchResults Array des View Controllers drei Elemente enthält.

Laufen Sie den Test. Er sollte ziemlich schnell erfolgreich sein, da es keine echte Netzwerkverbindung gibt!

Fake Update to Mock Object

Der vorherige Test hat einen Stub verwendet, um Input von einem gefälschten Objekt zu liefern. Als Nächstes werden Sie ein Scheinobjekt verwenden, um zu testen, dass Ihr Code UserDefaults korrekt aktualisiert wird.

Öffnen Sie das BullsEye-Projekt erneut. Die App hat zwei Spielstile: Der Benutzer bewegt entweder den Schieberegler, um den Zielwert zu erreichen, oder errät den Zielwert anhand der Schiebereglerposition. Ein segmentiertes Steuerelement in der unteren rechten Ecke schaltet den Spielstil um und speichert ihn in den Benutzereinstellungen.

Ihr nächster Test wird überprüfen, ob die App die gameStyle-Eigenschaft korrekt speichert.

Klicken Sie im Testnavigator auf Neue Einheitstestklasse und nennen Sie sie BullsEyeMockTests. Fügen Sie unterhalb der import-Anweisung Folgendes hinzu:

@testable import BullsEyeclass MockUserDefaults: UserDefaults { var gameStyleChanged = 0 override func set(_ value: Int, forKey defaultName: String) { if defaultName == "gameStyle" { gameStyleChanged += 1 } }}

MockUserDefaults überschreibt set(_:forKey:), um das gameStyleChanged-Flag zu erhöhen. Oft sieht man ähnliche Tests, die eine Bool-Variable setzen, aber das Inkrementieren einer Int-Variable gibt Ihnen mehr Flexibilität – zum Beispiel könnte Ihr Test prüfen, ob die Methode nur einmal aufgerufen wird.

Deklarieren Sie das SUT und das Mock-Objekt in BullsEyeMockTests:

var sut: ViewController!var mockUserDefaults: MockUserDefaults!

Ersetzen Sie als nächstes die Standardwerte setUp() und tearDown() durch die folgenden Werte:

override func setUp() { super.setUp() sut = UIStoryboard(name: "Main", bundle: nil) .instantiateInitialViewController() as? ViewController mockUserDefaults = MockUserDefaults(suiteName: "testing") sut.defaults = mockUserDefaults}override func tearDown() { sut = nil mockUserDefaults = nil super.tearDown()}

Dies erzeugt das SUT und das Mock-Objekt und injiziert das Mock-Objekt als eine Eigenschaft des SUT.

Ersetzen Sie nun die beiden Standard-Testmethoden in der Vorlage durch diese:

func testGameStyleCanBeChanged() { // given let segmentedControl = UISegmentedControl() // when XCTAssertEqual( mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions") segmentedControl.addTarget(sut, action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged) segmentedControl.sendActions(for: .valueChanged) // then XCTAssertEqual( mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")}

Die when-Aussage ist, dass das gameStyleChanged-Flag 0 ist, bevor die Testmethode die segmentierte Steuerung ändert. Wenn also die then-Behauptung ebenfalls wahr ist, bedeutet dies, dass set(_:forKey:) genau einmal aufgerufen wurde.

Starten Sie den Test; er sollte erfolgreich sein.

UI-Tests in Xcode

Mit UI-Tests können Sie Interaktionen mit der Benutzeroberfläche testen. UI-Tests funktionieren, indem Sie die UI-Objekte einer App mit Abfragen finden, Ereignisse synthetisieren und dann die Ereignisse an diese Objekte senden. Mit der API können Sie die Eigenschaften und den Zustand eines UI-Objekts untersuchen, um sie mit dem erwarteten Zustand zu vergleichen.

Fügen Sie im Testnavigator des BullsEye-Projekts ein neues UI-Testziel hinzu. Überprüfen Sie, dass das zu testende Ziel BullsEye ist, und akzeptieren Sie den Standardnamen BullsEyeUITests.

Öffnen Sie BullsEyeUITests.swift und fügen Sie diese Eigenschaft am Anfang der Klasse BullsEyeUITests hinzu:

var app: XCUIApplication!

Ersetzen Sie in setUp() die Anweisung XCUIApplication().launch() durch die folgende:

app = XCUIApplication()app.launch()

Ändern Sie den Namen von testExample() in testGameStyleSwitch().

Öffnen Sie eine neue Zeile in testGameStyleSwitch() und klicken Sie auf die rote Schaltfläche „Aufzeichnen“ am unteren Rand des Editorfensters:

Dadurch wird die App im Simulator in einem Modus geöffnet, der Ihre Interaktionen als Testbefehle aufzeichnet. Sobald die App geladen ist, tippen Sie auf das Dia-Segment des Spielstil-Schalters und auf die obere Beschriftung. Klicken Sie dann auf die Xcode-Schaltfläche „Aufzeichnen“, um die Aufzeichnung zu beenden.

Sie haben jetzt die folgenden drei Zeilen in testGameStyleSwitch():

let app = XCUIApplication()app.buttons.tap()app.staticTexts.tap()

Der Recorder hat Code erstellt, um die gleichen Aktionen zu testen, die Sie in der App getestet haben. Senden Sie einen Tipp an den Schieberegler und das Etikett. Sie werden diese als Grundlage für Ihren eigenen UI-Test verwenden.
Wenn Sie andere Anweisungen sehen, löschen Sie sie einfach.

Die erste Zeile dupliziert die Eigenschaft, die Sie in setUp() erstellt haben, also löschen Sie diese Zeile. Sie brauchen noch nichts zu tippen, löschen Sie also auch .tap() am Ende der Zeilen 2 und 3. Öffnen Sie nun das kleine Menü neben und wählen Sie segmentedControls.buttons.

Das, was übrig bleibt, sollte folgendes sein:

app.segmentedControls.buttonsapp.staticTexts

Tippen Sie auf andere Objekte, damit der Rekorder Ihnen hilft, den Code zu finden, auf den Sie in Ihren Tests zugreifen können. Ersetzen Sie nun diese Zeilen durch den folgenden Code, um einen bestimmten Abschnitt zu erstellen:

// givenlet slideButton = app.segmentedControls.buttonslet typeButton = app.segmentedControls.buttonslet slideLabel = app.staticTextslet typeLabel = app.staticTexts

Nachdem Sie nun Namen für die beiden Schaltflächen im segmentierten Steuerelement und die beiden möglichen oberen Beschriftungen haben, fügen Sie den folgenden Code hinzu:

// thenif slideButton.isSelected { XCTAssertTrue(slideLabel.exists) XCTAssertFalse(typeLabel.exists) typeButton.tap() XCTAssertTrue(typeLabel.exists) XCTAssertFalse(slideLabel.exists)} else if typeButton.isSelected { XCTAssertTrue(typeLabel.exists) XCTAssertFalse(slideLabel.exists) slideButton.tap() XCTAssertTrue(slideLabel.exists) XCTAssertFalse(typeLabel.exists)}

Dies prüft, ob die richtige Beschriftung vorhanden ist, wenn Sie tap() auf jede Schaltfläche im segmentierten Steuerelement tippen. Führen Sie den Test aus – alle Behauptungen sollten erfolgreich sein.

Leistungstests

Aus der Dokumentation von Apple: Bei einem Leistungstest wird ein Codeblock, den Sie bewerten möchten, zehnmal ausgeführt, wobei die durchschnittliche Ausführungszeit und die Standardabweichung für die einzelnen Durchläufe erfasst werden. Die Mittelung dieser einzelnen Messungen ergibt einen Wert für den Testlauf, der dann mit einer Basislinie verglichen werden kann, um Erfolg oder Misserfolg zu bewerten.

Es ist sehr einfach, einen Leistungstest zu schreiben: Sie platzieren einfach den Code, den Sie messen möchten, in die Schließung des measure().

Um dies in Aktion zu sehen, öffnen Sie das HalfTunes Projekt erneut und fügen Sie in HalfTunesFakeTests.swift, fügen Sie den folgenden Test hinzu:

func test_StartDownload_Performance() { let track = Track( name: "Waterloo", artist: "ABBA", previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a") measure { self.sut.startDownload(track) }}

Starten Sie den Test und klicken Sie dann auf das Symbol, das neben dem Anfang der measure()-Schließung erscheint, um die Statistiken zu sehen.

Klicken Sie auf Set Baseline, um eine Referenzzeit festzulegen. Führen Sie dann den Leistungstest erneut durch und sehen Sie sich das Ergebnis an – es kann besser oder schlechter als die Basislinie sein. Mit der Schaltfläche „Bearbeiten“ können Sie die Baseline auf dieses neue Ergebnis zurücksetzen.

Baselines werden pro Gerätekonfiguration gespeichert, so dass Sie denselben Test auf mehreren verschiedenen Geräten ausführen lassen können und jedes Gerät eine andere Baseline beibehält, die von der Prozessorgeschwindigkeit, dem Speicher usw. der jeweiligen Konfiguration abhängt.

Wenn Sie Änderungen an einer Anwendung vornehmen, die sich auf die Leistung der getesteten Methode auswirken könnten, führen Sie den Leistungstest erneut aus, um zu sehen, wie er im Vergleich zur Baseline abschneidet.

Codeabdeckung

Das Codeabdeckungstool zeigt Ihnen, welcher Anwendungscode tatsächlich von Ihren Tests ausgeführt wird, so dass Sie wissen, welche Teile des Anwendungscodes (noch) nicht getestet werden.

Um die Codeabdeckung zu aktivieren, bearbeiten Sie die Testaktion des Schemas und aktivieren Sie das Kontrollkästchen Abdeckung sammeln für auf der Registerkarte Optionen:

Alle Tests ausführen (Befehl-U), dann den Berichtsnavigator öffnen (Befehl-9). Wählen Sie Coverage unter dem obersten Punkt in der Liste:

Klicken Sie auf das Aufdeckungsdreieck, um die Liste der Funktionen und Closures in SearchViewController.swift zu sehen:

Scrollen Sie nach unten zu updateSearchResults(_:), um zu sehen, dass die Abdeckung 87,9% beträgt.

Klicken Sie auf die Pfeilschaltfläche für diese Funktion, um die Quelldatei der Funktion zu öffnen. Wenn Sie mit der Maus über die Abdeckungsbemerkungen in der rechten Seitenleiste fahren, werden Codeabschnitte grün oder rot hervorgehoben:

Die Abdeckungsbemerkungen zeigen, wie oft ein Test auf die einzelnen Codeabschnitte trifft; Abschnitte, die nicht aufgerufen wurden, sind rot hervorgehoben. Wie zu erwarten, wurde die for-Schleife 3 Mal ausgeführt, aber nichts in den Fehlerpfaden.

Um die Abdeckung dieser Funktion zu erhöhen, könnten Sie abbaData.json duplizieren und dann so bearbeiten, dass sie die verschiedenen Fehler verursacht. Ändern Sie z.B. "results" in "result" für einen Test, der print("Results key not found in dictionary") trifft.

100% Abdeckung?

Wie sehr sollten Sie nach 100% Codeabdeckung streben? Googeln Sie „100% Unit Test Coverage“ und Sie werden eine Reihe von Argumenten dafür und dagegen finden, zusammen mit einer Debatte über die eigentliche Definition von „100% Coverage“. Die Argumente dagegen besagen, dass die letzten 10-15% den Aufwand nicht wert sind. Die Argumente dafür besagen, dass die letzten 10-15% am wichtigsten sind, weil sie so schwer zu testen sind. Googeln Sie „schwer zu testendes, schlechtes Design“, um überzeugende Argumente dafür zu finden, dass nicht testbarer Code ein Zeichen für tiefere Designprobleme ist.

Wie geht es weiter?

Sie haben jetzt einige großartige Werkzeuge, die Sie beim Schreiben von Tests für Ihre Projekte verwenden können. Ich hoffe, dieses iOS Unit Testing und UI Testing Tutorial hat Ihnen das Selbstvertrauen gegeben, alles zu testen!

Sie können die fertige Version des Projekts über die Schaltfläche Download Materials am oberen oder unteren Rand dieses Tutorials herunterladen. Entwickeln Sie Ihre Fähigkeiten weiter, indem Sie zusätzliche eigene Tests hinzufügen.

Hier sind einige Ressourcen für weitere Studien:

  • Es gibt mehrere WWDC-Videos zum Thema Testen. Zwei gute von der WWDC17 sind: Engineering for Testability und Testing Tips & Tricks.
  • Der nächste Schritt ist die Automatisierung: Continuous Integration und Continuous Delivery. Beginnen Sie mit Apples Automatisieren des Testprozesses mit Xcode Server und xcodebuild und dem Wikipedia-Artikel über kontinuierliche Auslieferung, der sich auf das Fachwissen von ThoughtWorks stützt.
  • Wenn Sie bereits eine App haben, aber noch keine Tests dafür geschrieben haben, sollten Sie sich das Buch Working Effectively with Legacy Code von Michael Feathers zu Gemüte führen, denn Code ohne Tests ist Legacy Code!
  • Jon Reids Quality Coding Sample App Archives sind großartig, um mehr über testgetriebene Entwicklung zu erfahren.

raywenderlich.com Weekly

Der Newsletter von raywenderlich.com ist der einfachste Weg, um über alles, was Sie als mobiler Entwickler wissen müssen, auf dem Laufenden zu bleiben.

Holen Sie sich eine wöchentliche Zusammenfassung unserer Tutorials und Kurse und erhalten Sie als Bonus einen kostenlosen vertiefenden E-Mail-Kurs!

Durchschnittsbewertung

4.7/5

Bewertung für diesen Inhalt hinzufügen

Anmelden, um eine Bewertung hinzuzufügen

90 Bewertungen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.