iOS-Fortschritt testen: Gewichtung, Abbruch und Abschluss
Bei der Stapelverarbeitung von Dateien erreicht der Fortschrittsbalken bereits 100 Prozent, doch anschließend erscheint eine Fehlermeldung. Oder der Nutzer klickt auf Abbrechen, während im Hintergrund trotzdem noch Ergebnisse übernommen werden. Meist liegt das nicht an der Darstellung, sondern daran, dass Arbeitsumfang, Abbruchsignal und fachlicher Endzustand miteinander vermischt werden. Auf einem Cloud-Mac von GNUMac lassen sich diese Verträge zunächst mit Unit-Tests absichern, die weder Netzwerk noch Simulator benötigen. Erst danach folgt die Prüfung der iOS-Oberfläche.
Zuerst festlegen, was Fortschritt bedeutet
Als Test-Fixture dient hier die Aufgabe „vier Eingaben konvertieren, anschließend zwei Ausgaben prüfen“. Die Konvertierung erhält 60 Prozent des gesamten Arbeitsumfangs, die Prüfung 40 Prozent. Diese Gewichtung ist eine Vorgabe des Tests, kein Messergebnis zur Leistung und keine Aussage über die verbleibende Zeit.
Der übergeordnete Fortschritt erhält einen Gesamtumfang von 100 Einheiten. Die untergeordneten Fortschrittsobjekte zählen jeweils Dateien. Die Anzahl konvertierter Dateien darf nicht direkt mit hochgeladenen Bytes addiert werden: Unterschiedliche Einheiten müssen zuerst auf die Gewichtung im übergeordneten Fortschritt abgebildet werden.
Der fachliche Zustand wird separat modelliert: wartend, laufend, Abbruch läuft, abgebrochen, fehlgeschlagen und erfolgreich. Das Fortschrittsobjekt beschreibt ausschließlich Mengen; es ist nicht für die Klassifizierung von Fehlern zuständig.
Die Prozentzahl beantwortet die Frage „Wie viel des vereinbarten Arbeitsumfangs ist erledigt?“. Der fachliche Zustand beantwortet dagegen „Wurde das Ergebnis bereits zuverlässig bereitgestellt?“. Beides ist nicht austauschbar.
Vor der Implementierung werden drei Bedingungen festgehalten: Jedes untergeordnete Fortschrittsobjekt wird nur einmal eingebunden, die erledigte Menge darf die Gesamtmenge nicht überschreiten, und vor der endgültigen Übernahme des Ergebnisses muss eine Abbruchprüfung stattfinden. Übergeordnetes und untergeordnete Objekte gehören jeweils zu einem einzelnen Task und nicht in eine globale Variable, die sich alle Tasks teilen.
Die Gewichtung mit deterministischen Tests prüfen
Im Unit-Test-Target des bestehenden Projekts wird eine neue Testdatei mit folgendem Code angelegt. Der Test verwendet nur Foundation und XCTest. Er wartet weder auf echte Dateikonvertierungen noch versucht er, die Ausführungsreihenfolge durch feste Wartezeiten abzuschätzen.
import Foundation
import XCTest
final class WeightedProgressTests: XCTestCase {
func testWeightedStages() {
let root = Progress(totalUnitCount: 100)
let convert = Progress(totalUnitCount: 4)
let verify = Progress(totalUnitCount: 2)
root.addChild(convert, withPendingUnitCount: 60)
root.addChild(verify, withPendingUnitCount: 40)
XCTAssertEqual(root.fractionCompleted, 0, accuracy: 0.000001)
convert.completedUnitCount = 2
XCTAssertEqual(root.fractionCompleted, 0.3, accuracy: 0.000001)
verify.completedUnitCount = 1
XCTAssertEqual(root.fractionCompleted, 0.5, accuracy: 0.000001)
convert.completedUnitCount = 4
verify.completedUnitCount = 2
XCTAssertEqual(root.fractionCompleted, 1, accuracy: 0.000001)
}
}
Die beiden Phasen werden bewusst abwechselnd vorangebracht. So wird geprüft, dass der übergeordnete Fortschritt nicht nur die gerade aktive Phase abbildet. Gleitkommawerte werden mit einer Toleranz verglichen, nicht anhand von Prozentangaben, die zuvor als Zeichenfolge formatiert wurden.
Die Testklasse wird im Testnavigator auf dem Cloud-Mac ausgeführt. Commit-ID, Testergebnisse und Angaben zur verwendeten Toolchain werden dokumentiert. Gibt es bereits einen Testaufruf über die Kommandozeile, wird dort lediglich dieses Test-Target ergänzt. Für die Fortschrittstests wird keine eigene Simulator-Matrix aufgebaut.
Ein Abbruch erfordert kooperative Ausführung
Progress.cancel() setzt ein Signal, beendet aber keine laufende Schleife zwangsweise. Vor jeder Arbeitseinheit muss der Zustand geprüft werden; der Zähler darf erst steigen, wenn die Einheit tatsächlich abgeschlossen ist. Unmittelbar vor dem endgültigen Ersetzen der Ausgabedatei ist eine weitere Prüfung erforderlich.
Der folgende Test prüft die Bedingung „Nach dem Abbruch wird keine weitere Arbeitseinheit gestartet“. Er erhebt nicht den Anspruch, auch Race Conditions bei nebenläufiger Ausführung abzudecken, und kann direkt in die vorherige Testklasse aufgenommen werden.
func testCancellationStopsNextUnit() {
let progress = Progress(totalUnitCount: 4)
progress.isCancellable = true
var processed = 0
func runNextUnit() {
guard !progress.isCancelled else { return }
processed += 1
progress.completedUnitCount += 1
}
runNextUnit()
progress.cancel()
runNextUnit()
XCTAssertTrue(progress.isCancelled)
XCTAssertEqual(processed, 1)
XCTAssertEqual(progress.completedUnitCount, 1)
}
Für die asynchrone Ausführung werden zusätzlich zwei kontrollierbare Haltepunkte benötigt: vor dem Start einer zeitaufwendigen Operation und vor der endgültigen Übernahme des Ergebnisses. Der Test hält den Task an, löst den Abbruch aus und gibt die Ausführung anschließend wieder frei. Dann prüft er, dass keine neue Arbeit beginnt und kein Endergebnis veröffentlicht wird. Die Koordination erfolgt über Test-Gates, nicht über feste Wartezeiten.
Bietet die zugrunde liegende Operation eine Abbruchschnittstelle, muss das Signal auch dorthin weitergereicht werden. Lässt sie sich nicht unterbrechen, wird ihre Rückkehr abgewartet; anschließend werden das Ergebnis verworfen und temporäre Dateien entfernt. Die Oberfläche bleibt währenddessen im Zustand „Abbruch läuft“, statt vorzeitig einen bereits erfolgten Stopp zu melden.
Unbekannten Umfang und Wiederholungen nicht kaschieren
Solange die Gesamtzahl der Dateien noch ermittelt wird, zeigt die Oberfläche zunächst „Dateien werden gezählt“ an. Aus einer Gesamtmenge von null wird keine Prozentzahl erzwungen. Nach Abschluss des Scans wird die Aufgabenliste festgeschrieben. Erst dann werden alle über- und untergeordneten Fortschrittsobjekte verknüpft und die Verarbeitung gestartet.
Wird der Nenner während der Ausführung ständig erhöht, kann der Fortschritt zurückgehen. Müssen Aufgaben tatsächlich dynamisch entdeckt werden, sollte die Oberfläche den Phasenstatus oder die bereits verarbeitete Menge anzeigen. Dabei muss klar sein, dass eine angezeigte Prozentzahl nur für die bereits festgelegte Phase gilt.
Auch bei Wiederholungsversuchen darf dasselbe untergeordnete Fortschrittsobjekt nicht immer wieder auf null gesetzt werden. Die fachliche Ebene kann die Nummer des aktuellen Versuchs festhalten; die Fortschrittsebene summiert dagegen erfolgreich abgeschlossene logische Einheiten. Ein fehlgeschlagener Versuch bedeutet nicht, dass eine weitere Datei erledigt wurde.
Für eine leere Eingabe braucht es ebenfalls eine eigene Regel: Ist sie zulässig, wird unmittelbar ein erfolgreiches Ergebnis mit „Keine Verarbeitung erforderlich“ zurückgegeben. Muss der Aufrufer Eingaben liefern, wird der Fehler bereits vor dem Start gemeldet. Es wird kein Task angelegt, der dauerhaft bei null stehen bleibt.
Oberfläche anbinden und dauerhaft prüfen
Nach dem Beobachten einer Fortschrittsänderung muss die Aktualisierung des Oberflächenzustands auf dem Main Actor erfolgen. Jeder Durchlauf erhält eine eigene Task-ID. Beim Zurückschreiben wird diese ID geprüft, damit verspätete Benachrichtigungen eines vorherigen Tasks den neuen Fortschritt nicht überschreiben.
Die angezeigte Prozentzahl darf auf eine ganze Zahl gerundet werden. Der gerundete Wert darf jedoch nicht darüber entscheiden, ob die Aufgabe erfolgreich war. Sind nach der letzten verarbeiteten Datei noch eine Prüfung und eine atomare Übernahme erforderlich, muss dafür Arbeitsumfang reserviert werden. Bei einem Fehler bleibt der Fehlerzustand erhalten, statt den Zähler künstlich auf den Gesamtwert zu setzen.
Vor dem Zusammenführen der Änderungen wird jeder Punkt geprüft:
- Die Assertions für Zwischenwerte der Gewichtung und das abwechselnde Voranschreiten der Phasen bestehen; das Ergebnis hängt nicht von der Ausführungsgeschwindigkeit ab.
- Für einen Abbruch vor dem Start, während der Verarbeitung und unmittelbar vor der Ergebnisübernahme gibt es jeweils eigene Testfälle.
- Fehlerpfade werden nicht als Erfolg gewertet, und temporäre Ausgaben lassen sich entfernen.
- Nach dem Start eines neuen Tasks können Benachrichtigungen des alten Tasks die aktuelle Oberfläche nicht mehr verändern.
- Für leere Eingaben, einen unbekannten Gesamtumfang und Wiederholungsversuche gelten eindeutige Anzeigeregeln.
Das eigentliche Ergebnis ist kein Fortschrittsbalken, der nur möglichst gleichmäßig läuft, sondern ein überprüfbarer Zustandsvertrag. Zuerst muss die Zählung verlässlich sein, dann der Abbruch kontrollierbar. Und Erfolg darf erst dann gemeldet werden, wenn das Ergebnis tatsächlich nutzbar ist.
Häufig gestellte Fragen
Bedeutet ein Fortschritt von 100 Prozent automatisch Erfolg?
Nein. Der Zähler beschreibt erledigte Arbeitseinheiten. Validierung und Übergabe des Ergebnisses müssen in einem eigenen fachlichen Zustand erfasst werden.
Stoppt cancel() laufende Arbeit automatisch?
Nein. Der ausführende Code muss den Abbruch prüfen, an die eigentliche Operation weitergeben und vor der endgültigen Übernahme erneut kontrollieren.
Einen dedizierten Mac mini für Ihren Workflow konfigurieren
Wählen Sie Modell, Standort und Laufzeit passend zu Ihrem Projekt. Prüfen Sie zuerst Arbeitsspeicher, Speicherplatz und Verbindung. Der tatsächlich verfügbare Status wird Ihnen live in der Konsole angezeigt.