5. Dezember 2017

Automatisiertes Testen mit XCTest

Tutorial 1: Unit-Test für FlashcardsModel erstellen und ausführen

  1. Laden Sie den Start-Stand von dem Flashcards-Beispielprojekt: Flashcards.zip.

  2. Erstellen Sie via File » New » Target... ein neues Target iOS Unit Testing Bundle:

    iOS Unit Testing Bundle erstellen
  3. Machen Sie sich mit dem Test-Target vertraut - das Test-Target verweist auf die App des Flashcards-Target und kann die Klassen daraus testen:

    Xcode Test Target
  4. Öffnen Sie die bestehende Testklasse FlashcardsTest und prüfen Sie, dass diese nur im FlashcardsTest-Target enthalten ist:

    Xcode Test Target
  5. Bauen und führen Sie die Tests aus mit Product » Test ⌘U. Verwenden Sie in diesem Kapitel immer diese Funktion, um das Projekt zu bauen und auszuführen. Hinweis: Mit ⌘B wird nur das App-Target gebaut, nicht jedoch das Test-Target. Um das Test-Target zu bauen, ohne es auszuführen, verwenden Sie Product » Build For » Testing ⌘⇧U.

  6. Machen Sie die Klasse FlashcardsModel testbar, indem Sie mit einem zweiten Initializer die Möglichkeit schaffen, einen beliebigen NSPersistentContainer von außen zu konfigurieren:

    class FlashcardsModel {
    
        static let shared = FlashcardsModel()
    
        let persistentContainer : NSPersistentContainer
    
        private init() {
            self.persistentContainer = NSPersistentContainer(defaultContainerWithName: "Flashcards")
        }
    
        init(persistentContainer : NSPersistentContainer) {
        self.persistentContainer = persistentContainer
    }
        
        // ...
        
    }
  7. Initialisieren Sie in der setUp-Methode der Testklasse ein FlashcardsModel mit einer In-Memory-Core-Data-Datenbank und legen Sie in der Datenbank einige Testdaten an:

    import XCTest
    import CoreData
    @testable import Flashcards
    
    class FlashcardsTests: XCTestCase {
        var model : FlashcardsModel!
    var card : Card!
    
        override func setUp() {
            super.setUp()
            let persistentContainer = NSPersistentContainer(defaultContainerWithName: "Flashcards", inMemory: true)
    self.model = FlashcardsModel(persistentContainer: persistentContainer)
    self.card = self.model.createCard()
        }
    }

    Hinweis: Das App-Modul wird in der Test-Klasse mit @testable importiert. Dieser Import bewirkt, dass der Testcode auch auf nicht-öffentliche Deklarationen des importierten Moduls zugreifen kann.

  8. Entfernen Sie den verbleibenden Code in der Testklasse (tearDown/testExample/testPerformanceExample).

  9. Implementieren Sie eine Hilfsmethode in der Testklasse, die einen Date aus einem formatierten String parst:

    func dateFor(_ string : String) -> Date {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
        return dateFormatter.date(from: string)!
    }
  10. Implementieren Sie eine Test-Methode testIncorrectScheduled1HourLater, so dass eine Methode Card.answeredCorrect für eine inkorrekte Antwort aufgerufen wird und geprüft wird, dass die Lernkarte zur Wiederholung frühestens nach einer Stunde markiert wird:

    func testIncorrectScheduled1HourLater() {
        card.answered(correct:false, date:dateFor("2015-01-05 15:00"))
        XCTAssertEqual(card.scheduleDate, dateFor("2015-01-05 16:00"), "scheduleDate +1h")
    }
  11. Deklarieren Sie in Card+CoreDataClass.swift die Methode Card.answered(correct:date:) zunächst ohne Implementierung:

    class Card : NSManagedObject {
        // ...
    
        func answered(correct : Bool, date : Date) {
        }
    }
  12. Führen Sie mit Product » Test ⌘U die Tests aus und prüfen Sie, dass der Test fehlschlägt:

    Fehlgeschlagener Testfall

  13. Implementieren Sie die answered-Methode und verwenden Sie Calendar.current.date(byAdding: ...) für die Berechnung des Datumswertes:

    import CoreData
    
    class Card: NSManagedObject {
    
        static let entityName = "Card"
        
        func answered(correct : Bool, date : Date) {
            self.scheduleDate = Calendar.current.date(byAdding: DateComponents(hour: 1), to: date)
        }
    }
  14. Führen Sie die Tests erneut aus und prüfen Sie, dass die Testbedingung nun erfüllt ist:

    Test erfolgreich
  15. Ergänzen Sie einen Testfall, der prüft, dass die Lernkarte bei einer korrekten Antwort erst am nächsten Tag wiederholt wird:

    func testCorrectScheduledTomorrow() {
        card.answered(correct:true, date:dateFor("2015-01-05 15:00"))
        XCTAssertEqual(card.scheduleDate, dateFor("2015-01-06 15:00"), "scheduleDate +1d")
    }
  16. Implementieren Sie die answeredCorrect-Methode entsprechend:

    func answered(correct : Bool, date : Date) {
        let interval = correct ? DateComponents(day: 1) : DateComponents(hour: 1)
        self.scheduleDate = Calendar.current.date(byAdding: interval, to: date)
    }
  17. Führen Sie die Tests erneut mit Product » Test ⌘U aus.

  18. Committen Sie Ihre Änderungen: „21.1. Test für Card.answered“.

Tutorial 2: Kartenstapel-Download mit einem UI-Test testen

  1. Öffnen Sie das Storyboard und vergeben Sie für die Buttons im CardViewController Accessibility Labels „Inkorrekt“, „Antwort zeigen“ und „Korrekt“ (diese würden für einen blinden Anwender von einem Screenreader vorgelesen werden und können im Test verwendet werden, um grafische Bedienelemente zu identifizieren):

    Accessibility Labels
  2. Implementieren Sie im FlashcardsModel eine Methode clear, um die Daten der App zurückzusetzen:

    class FlashcardsModel {
        
        // ...
        
        func clear() {
        self.cards.forEach(self.viewContext.delete)
        save()
    }
    
    }
  3. Lösen Sie das Zurücksetzen der Daten im AppDelegate aus, wenn die App mit der Option --clear gestartet wird:

    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        // ...
        
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            
            if ProcessInfo.processInfo.arguments.contains("--clear") {
        FlashcardsModel.shared.clear()
    }
            
            return true
        }
        
    }
  4. Erstellen Sie via File » New » Target... ein neues Target iOS Unit Testing Bundle.

  5. Öffnen Sie FlashcardsUITests.swift. Passen Sie die setUp-Methode so an, dass die Daten vor jedem Testfall zurückgesetzt werden:

    class FlashcardsUITests: XCTestCase {
        
        override func setUp() {
            super.setUp()
            
            continueAfterFailure = false
    
            let app = XCUIApplication()
    app.launchArguments = ["--clear"]
    app.launch()
        }
    
    }
  6. Nennen Sie die Methode testExample in testFlashcardDownload um. Setzen Sie den Cursor in die Methodenimplementierung und starten Sie mit Record UI Test die Aufzeichnung eines UI-Tests:

    Record UI Test
  7. Laden Sie in der App den Kartenstapel Say Hello Around the World herunter und lernen Sie die Lernkarten des Kartenstapels, um entsprechenden Testcode aufzuzeichnen und beenden Sie die Ausführung.

  8. Passen Sie den Test-Code so an, dass der Kartenstapel heruntergeladen und am Ende auf den korrekten Titel „Noch 2 Lernkarten“ geprüft wird:

    func testFlashcardDownload() {
    
        let app = XCUIApplication()
        app.navigationBars.buttons["Add"].tap()
    
        app.tables.staticTexts["Say Hello Around the World"].tap()
    
        app.buttons["3 Karten lernen"].tap()
        
        app.buttons["Antwort zeigen"].tap()
        app.buttons["Korrekt"].tap()
    
        XCTAssertTrue(app.navigationBars["Noch 2 Lernkarten"].exists)
    }
  9. Führen Sie die Tests mit Product » Test ⌘U aus.

  10. Committen Sie Ihre Änderungen: „21.2. UI-Test für Kartenstapel-Download“.