iOS-Such-Debounce auf dem Cloud-Mac mit virtueller Zeit testen

iOS-Such-Debounce auf dem Cloud-Mac mit virtueller Zeit testen

iOS-Such-Debounce auf dem Cloud-Mac mit virtueller Zeit testen

Für das Suchfeld ist eine Debounce-Verzögerung von 300 Millisekunden vorgesehen. Der Test lautet aber: „Nach der Eingabe eine halbe Sekunde warten und dann die Anzahl der Anfragen prüfen.“ Bei parallel laufenden Aufgaben auf einem Cloud-Mac ist ein solcher Test nicht nur langsam, sondern auch anfällig für Verzögerungen durch die Ablaufplanung. Geprüft werden müssen eigentlich drei Regeln: der Fälligkeitszeitpunkt nach der letzten Eingabe, das Ersetzen älterer Eingaben und das Ausbleiben einer Auslieferung nach dem Leeren des Felds. Werden diese Regeln vom Timer getrennt, müssen die Tests keine reale Zeit mehr abwarten.

Zuerst den zeitlichen Vertrag der Suche festlegen

Dieser Artikel verwendet ein Debounce-Verfahren mit Ausführung am Ende der Ruhephase: Jede nicht leere Eingabe setzt den Fälligkeitszeitpunkt neu. Sobald dieser erreicht ist, wird ausschließlich der neueste Suchbegriff genau einmal ausgeliefert. Führende und nachfolgende Leerraumzeichen werden vorher entfernt; sowohl reine Leerraumeingaben als auch leere Zeichenfolgen verwerfen den noch ausstehenden Suchbegriff. Auch die erneute Eingabe desselben Begriffs startet die Wartezeit von vorn. Das ist eine ausdrücklich festgelegte Produktregel, kein Standardverhalten jeder Implementierung.

Die Zeit wird als ganzzahlige Anzahl von Millisekunden seit Testbeginn dargestellt, nicht als Systemdatum. Im Produktivbetrieb sollte die verstrichene Zeit von einer monotonen Uhr stammen. Die Systemzeit ist ungeeignet, da sie bei einer Uhrzeitkorrektur springen kann. Die 300 Millisekunden sind lediglich eine beispielhafte Vorgabe, keine Leistungskennzahl des Dienstes.

Debounce bestimmt nur, wann ein Suchbegriff ausgeliefert wird. Ob bereits gestartete Netzwerkanfragen abgebrochen werden und ob ältere Antworten die Oberfläche aktualisieren dürfen, sind zwei weitere, eigenständige Verträge. „Der Timer wurde abgebrochen“ ersetzt deren Prüfung nicht.

Die Zeitregeln als eigenständigen Zustandsautomaten abbilden

Erstellen Sie auf dem Cloud-Mac über die Kommandozeile ein kleines Paket zur Überprüfung. Die aktuell ausgewählte Entwicklungs-Toolchain muss Swift enthalten; ein Simulator oder eine grafische Sitzung ist hierfür nicht erforderlich.

mkdir SearchTiming
cd SearchTiming
swift package init --type library --name SearchTiming

Ersetzen Sie Sources/SearchTiming/SearchTiming.swift durch den folgenden Inhalt. Der Code erstellt weder Threads noch Timer. Der Aufrufer muss Zeitwerte in nicht absteigender Reihenfolge übergeben und seriell auf den Zustand zugreifen.

import Foundation

struct SearchDebounce {
    let delay: Int64
    private(set) var deadline: Int64?
    private var pending: String?

    init(delay: Int64 = 300) {
        precondition(delay > 0)
        self.delay = delay
    }

    mutating func input(_ text: String, at now: Int64) {
        let query = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !query.isEmpty else {
            pending = nil
            deadline = nil
            return
        }
        pending = query
        deadline = now + delay
    }

    mutating func takeDue(at now: Int64) -> String? {
        guard let limit = deadline, now >= limit else {
            return nil
        }
        let query = pending
        pending = nil
        deadline = nil
        return query
    }
}

takeDue prüft den Fälligkeitszeitpunkt und entnimmt zugleich den ausstehenden Suchbegriff. Wiederholte Rückrufe führen daher nicht zu einer mehrfachen Auslieferung, und ein zu früher Rückruf löscht den Suchbegriff nicht. Das Beispiel setzt Zeitwerte im normalen Betriebsbereich voraus. Nimmt der Uhradapter externe Werte entgegen, sollten zusätzlich negative Werte, rückwärts laufende Zeitwerte und Überläufe bei der Addition geprüft werden.

Virtuelle Zeit voranschreiten lassen statt sleep aufzurufen

Ersetzen Sie Tests/SearchTimingTests/SearchTimingTests.swift durch die folgenden Tests. Jeder Testfall legt einen neuen Zustand an, damit weder eine Uhr noch ausstehende Eingaben zwischen den Tests geteilt werden.

import XCTest
@testable import SearchTiming

final class SearchTimingTests: XCTestCase {
    func testDeadlineAndSingleDelivery() {
        var state = SearchDebounce()
        state.input(" swift ", at: 0)
        XCTAssertNil(state.takeDue(at: 299))
        XCTAssertEqual(state.takeDue(at: 300), "swift")
        XCTAssertNil(state.takeDue(at: 301))
    }

    func testLatestInputMovesDeadline() {
        var state = SearchDebounce()
        state.input("s", at: 0)
        state.input("sw", at: 100)
        state.input("swift", at: 220)
        XCTAssertEqual(state.deadline, 520)
        XCTAssertNil(state.takeDue(at: 300))
        XCTAssertNil(state.takeDue(at: 519))
        XCTAssertEqual(state.takeDue(at: 520), "swift")
    }

    func testBlankCancelsPendingInput() {
        var state = SearchDebounce()
        state.input("swift", at: 0)
        state.input(" \n ", at: 100)
        XCTAssertNil(state.deadline)
        XCTAssertNil(state.takeDue(at: 1000))
    }

    func testSameInputRestartsDelay() {
        var state = SearchDebounce()
        state.input("swift", at: 0)
        state.input("swift", at: 200)
        XCTAssertNil(state.takeDue(at: 300))
        XCTAssertEqual(state.takeDue(at: 500), "swift")
    }
}

Führen Sie swift test aus. Schlägt eine Assertion fehl, muss der Befehl einen Exitcode ungleich null zurückgeben. Die Pipeline darf nicht lediglich die Ausgabe archivieren und anschließend fortfahren. Die Tests decken den Zeitpunkt unmittelbar vor der Fälligkeit, die exakte Fälligkeit und die wiederholte Entnahme ab. Ein zusätzlicher Testfall mit einem Zeitpunkt deutlich nach der Fälligkeit kann prüfen, dass eine verzögerte Ausführung den Suchbegriff nicht dauerhaft verloren gehen lässt.

Bei der Anbindung realer Tasks zwei Abbruchfehler verhindern

Für die tatsächliche Anbindung lässt sich mit ContinuousClock bis zum Fälligkeitszeitpunkt warten. Der Zustandsautomat verwaltet die Regeln, der Adapter sorgt für das Aufwecken. Bei jeder Eingabe wird zuerst der Zustand aktualisiert, anschließend der bisherige wartende Task abgebrochen und für den neuen Fälligkeitszeitpunkt ein neuer wartender Task angelegt. Nach dem Leeren des Felds wird kein neuer Task erstellt.

Der erste Fehler besteht darin, einen Abbruchfehler zu verschlucken. Wird nach einem mit try? ausgeführten Warten direkt die Auslieferungslogik aufgerufen, läuft auch der Abbruchpfad weiter. Beim Abfangen eines Abbruchs muss die Ausführung beendet werden. Ein Abbruch darf nicht wie „die Wartezeit ist abgelaufen“ behandelt werden.

Der zweite Fehler sind verspätete Rückrufe älterer Tasks. Vergeben Sie für jede Eingabe eine neue Generationsnummer. Der wartende Task speichert die Generation, mit der er erstellt wurde. Nach dem Aufwachen vergleicht er sie auf demselben seriellen Executor mit der aktuellen Generation. Nur bei Übereinstimmung darf er den ausstehenden Suchbegriff entnehmen. Allein zu prüfen, ob der Task abgebrochen wurde, reicht nicht aus: Abbruch und Aufwachen können gleichzeitig eintreten.

Oberflächeneingaben, die Aktualisierung der Generation und die Auslieferung des Ergebnisses können gemeinsam auf dem Main Executor erfolgen. Lesen Sie den Zustand nicht im Hintergrund aus, um ihn anschließend nach einem asynchronen Wechsel zum Main Executor zu ändern. Sonst entsteht eine Lücke zwischen Prüfung und Entnahme. Die Millisekunden in den virtuellen Tests dienen lediglich dazu, Assertions einfach zu formulieren. Für das reale Warten sollten die Zeitpunkt- und Dauertypen der Uhr verwendet werden, damit sich keine Umrechnungsfehler aufsummieren.

Mit wenigen Integrationstests abschließend prüfen

Sobald die reinen Logiktests erfolgreich sind, prüfen Sie den realen Adapter, statt sämtliche Testfälle wieder auf lange Wartezeiten umzustellen. Wählen Sie einen ausreichend großzügigen Test-Timeout, prüfen Sie aber weiterhin, dass keine vorzeitige Auslieferung erfolgt und letztlich genau einmal ausgeliefert wird. Der Timeout soll nur hängen gebliebene Abläufe erkennen und nicht als exakte Zeit-Assertion dienen.

Die Abnahmecheckliste sollte mindestens Folgendes enthalten: Bei schnell aufeinanderfolgenden Eingaben wird nur die letzte ausgeliefert; das Leeren des Felds verwirft den ausstehenden Suchbegriff sofort; ein Abbruchfehler führt nicht in den Auslieferungspfad; Rückrufe älterer Generationen werden verworfen; nach der Freigabe der Oberfläche erfolgen keine Aktualisierungen mehr; mehrere aufeinanderfolgende Testdurchläufe liefern konsistente Ergebnisse. Falls die Anwendung identische Suchbegriffe deduplizieren soll, ist dafür eine separate Schicht vorzusehen. Dabei muss ausdrücklich festgelegt werden, ob derselbe Begriff nach dem Leeren und erneuten Eingeben wieder eine Suche auslösen darf.

Es genügt, die Commit-Version, die Toolchain-Version und den Exitcode der Tests festzuhalten. Tatsächliche Suchbegriffe der Nutzer müssen nicht protokolliert werden. Das Ergebnis ist kein Skript nach dem Muster „nach einer halben Sekunde klappt es vermutlich“, sondern eine Reihe von Assertions, die genau benennen können, welche zeitliche Regel verletzt wurde.

Häufig gestellte Fragen

Ersetzen Tests mit virtueller Zeit alle Tests mit echten Timern?

Nein. Sie prüfen die Regeln des Zustandsautomaten. Integrationstests müssen zusätzlich monotone Zeitmessung, Timer-Abbruch und die Übergabe auf dem vorgesehenen Executor prüfen.

Bricht ein leeres Suchfeld bereits laufende Anfragen ab?

Nein. Der Automat verwirft nur noch nicht übergebene Suchbegriffe. Laufende Netzwerkanfragen müssen separat abgebrochen und veraltete Antworten anhand einer Anfragegeneration verworfen werden.

Ein eigener Mac für Ihre Projekte

Mac-mini-Hardware, keine virtuelle Maschine. Mieten Sie tage-, wochen-, monats- oder quartalsweise. Wählen Sie zuerst Einsatzzweck und Ausstattung und prüfen Sie anschließend die aktuelle Verfügbarkeit direkt in der Konsole.

Modell und Mietdauer wählen Alle Preise ansehen