Ein Benutzer bearbeitet auf einem Cloud-Mac ein noch nicht übermitteltes Formular, wechselt zur Recherche in eine andere App, und wenige Minuten später beendet das System den Prozess. Beim erneuten Öffnen der App ist ein Absturz nicht das schlimmste Ergebnis: Problematischer ist eine scheinbar intakte Oberfläche, während der Entwurf gelöscht wurde, die Navigation zur Startseite zurückgesprungen ist oder ein inzwischen ungültiges Objekt wiederhergestellt wurde. Solche Fehler lassen sich mit gewöhnlichen Unit-Tests nur schwer abdecken. Besser geeignet ist XCUITest, um den vollständigen Lebenszyklus in einem isolierten Simulator nachzustellen.
Zuerst festlegen, welcher Zustand wiederhergestellt werden soll
„Die letzte Ansicht wiederherstellen“ sollte nicht als einzelne Anforderung behandelt werden. Vor dem Schreiben der Tests empfiehlt es sich, den Zustand in drei Kategorien zu unterteilen:
| Zustandstyp | Beispiele | Erwartung nach dem Neustart |
|---|---|---|
| Fachlicher Zustand | Entwurfstext, Filterkriterien, ausgewähltes Element | Gemäß den Produktregeln wiederherstellen |
| Navigationszustand | Aktuelle Seite, Tab, Listenposition | Bis zur weiterhin gültigen Ebene wiederherstellen |
| Temporärer Zustand | Ladeanimation, Dialog, Einmal-Token | Verwerfen und neu berechnen |
Die Wiederherstellungslogik muss außerdem zwischen einem regulären Wechsel in den Hintergrund und einer unerwarteten Prozessbeendigung unterscheiden. Im ersten Fall können mehr Details der Benutzeroberfläche erhalten bleiben; im zweiten hat die Datenkonsistenz Vorrang. Wenn ein Entwurf von einem bereits gelöschten Objekt abhängt, sollte die App zu einer sicheren Seite zurückkehren und den Grund erklären, statt eine ungültige Ansicht zwangsweise zu rekonstruieren.
Das Ziel von Tests zur Zustandswiederherstellung besteht nicht darin, jedes Pixel an seine vorherige Position zurückzusetzen. Entscheidend ist, dass Benutzer ihre Arbeit fortsetzen können und keine veralteten, unzulässigen oder nicht übermittelbaren Daten sehen.
Für jedes Szenario sollten vier Punkte dokumentiert werden: Ausgangszustand, Beendigungsaktion, Wiederherstellungsprüfungen und unzulässige Zustände. Bei einem fehlgeschlagenen Test kann das Team dadurch leichter erkennen, ob die Ursache in der Persistenz, im Routing oder in der Datenvalidierung liegt.
Deterministische Einstiegspunkte für die Automatisierung vorbereiten
UI-Tests dürfen nicht von Daten abhängen, die ein vorheriger Lauf hinterlassen hat. Über ausschließlich für Tests vorgesehene Startargumente kann die App fest definierte Fixtures importieren und die Daten in einen separaten Container schreiben. Die Namen der Fixtures sollten das fachliche Szenario beschreiben und keine veränderlichen Primärschlüssel aus der Datenbank verwenden.
let arguments = ProcessInfo.processInfo.arguments
if arguments.contains("-ui-testing"),
let index = arguments.firstIndex(of: "-fixture"),
arguments.indices.contains(index + 1) {
let fixture = arguments[index + 1]
try TestFixtureLoader.load(named: fixture)
}
Produktions-Builds müssen diesen Testeinstieg ignorieren oder vollständig entfernen. Noch zuverlässiger ist es, den Loader mithilfe von Build-Bedingungen auf interne Testkonfigurationen zu beschränken. Vor Testbeginn sollten außerdem Benachrichtigungen, Caches und gemeinsam verwendete Einstellungen gelöscht werden. Andernfalls kann ein zufällig noch vorhandener alter Entwurf zu einem falsch positiven Ergebnis führen.
Auch bei der Ausführung auf dedizierten physischen Nodes von Runner sollte jede Pipeline einen eigenen Simulator-Gerätesatz erhalten. Exklusive Rechenressourcen bedeuten nicht automatisch, dass auch die Testdaten isoliert sind. Verwenden zwei parallele Jobs denselben Simulator, können sie die App-Container des jeweils anderen weiterhin überschreiben.
Prozessbeendigung und Neustart mit XCUITest nachstellen
Der folgende Test erstellt zunächst einen Entwurf, versetzt die App in den Hintergrund, beendet den Prozess und startet die App erneut. Alle wichtigen Steuerelemente sollten stabile Accessibility Identifier besitzen, damit die Elementsuche nicht von lokalisierten Texten abhängt.
func testDraftRestoresAfterTermination() {
let app = XCUIApplication()
app.launchArguments = [
"-ui-testing",
"-fixture", "empty-project",
"-reset-state", "YES"
]
app.launch()
app.buttons["project.create"].tap()
let editor = app.textViews["draft.editor"]
editor.tap()
editor.typeText("release checklist")
app.buttons["draft.save"].tap()
XCUIDevice.shared.press(.home)
app.terminate()
app.launchArguments = [
"-ui-testing",
"-fixture", "empty-project"
]
app.launch()
XCTAssertTrue(app.navigationBars["draft.screen"].waitForExistence(timeout: 8))
XCTAssertEqual(app.textViews["draft.editor"].value as? String,
"release checklist")
XCTAssertTrue(app.staticTexts["restoration.completed"].exists)
}
Beim zweiten Start darf derselbe Test das Argument zum Zurücksetzen nicht erneut übergeben. Andernfalls löscht der Test selbst den Zustand, den er prüfen soll. Es reicht auch nicht aus, lediglich die Existenz des Editors zu bestätigen. Zusätzlich sollten Inhalt, Navigationstitel, ausgewähltes Objekt und Wiederherstellungshinweis geprüft werden.
Drei Startpfade getrennt testen
Mindestens drei unabhängige Testfälle sollten erhalten bleiben:
- Die App direkt aus dem Hintergrund in den Vordergrund holen und prüfen, dass eine kurze Unterbrechung nicht die gesamte Seite neu aufbaut.
- Nach dem Speichern des Zustands den Prozess beenden und prüfen, dass die Arbeit nach dem Neustart fortgesetzt werden kann.
- Ein ungültiges Objekt erzeugen, die App neu starten und prüfen, dass sie kontrolliert auf einen sicheren Zustand zurückfällt und die ungültige Route entfernt.
Jeder Testfall sollte nur einen Lebenszyklus prüfen. So lässt sich die Ursache eines Fehlers direkter bestimmen als bei einem langen Test mit mehr als einem Dutzend Schritten.
Simulator und Arbeitsverzeichnisse auf dem Cloud-Mac isolieren
Automatisierungsjobs sollten ausdrücklich einen temporären Gerätesatz erstellen, statt den Simulator im Standardverzeichnis des angemeldeten Benutzers zu verwenden. Wird der Gerätesatz nach Abschluss des Jobs gelöscht, werden zugleich die App-Container und verbliebene Snapshots bereinigt.
set -euo pipefail
DEVICE_SET="$RUNNER_TEMP/CoreSimulator"
RESULTS="$RUNNER_TEMP/StateRestoration.xcresult"
mkdir -p "$DEVICE_SET"
xcrun simctl --set "$DEVICE_SET" create \
"State-Restore-iPhone" \
"com.apple.CoreSimulator.SimDeviceType.iPhone-16" \
"com.apple.CoreSimulator.SimRuntime.iOS-18-0"
UDID="$(xcrun simctl --set "$DEVICE_SET" list devices \
available -j | /usr/bin/python3 scripts/first_device.py)"
xcrun simctl --set "$DEVICE_SET" boot "$UDID"
xcodebuild test \
-workspace Example.xcworkspace \
-scheme ExampleUITests \
-destination "platform=iOS Simulator,id=$UDID" \
-resultBundlePath "$RESULTS"
xcrun simctl --set "$DEVICE_SET" shutdown "$UDID" || true
rm -rf "$DEVICE_SET"
Die Runtime und der Gerätetyp aus dem Beispiel müssen durch Versionen ersetzt werden, die auf dem Node installiert sind. Zur Kontrolle können zunächst xcrun simctl list runtimes und xcrun simctl list devicetypes ausgeführt werden. Das Skript sollte nicht stillschweigend die „neueste“ Runtime auswählen. Nach einem Xcode-Update könnten sich sonst Screenshots, Systemdialoge und das Wiederherstellungsverhalten gleichzeitig ändern.
Fehlerbelege in diagnostizierbare Ergebnisse verwandeln
Fehler bei der Zustandswiederherstellung treten meist nach dem zweiten Start auf. Deshalb müssen Belege vor und nach der Wiederherstellung aufbewahrt werden. Empfehlenswert sind Screenshots nach wichtigen Aktionen. Außerdem sollten der verwendete Fixture-Name, die Simulator-ID, die App-Version und die Wiederherstellungsphase als XCTest Attachments gespeichert werden. Bei fehlgeschlagenen Tests sollte die .xcresult-Datei erhalten bleiben; für erfolgreiche Jobs kann die Aufbewahrungsdauer gemäß den Teamrichtlinien verkürzt werden.
Die Reihenfolge der Fehlersuche lässt sich standardisieren:
- Prüfen, ob der erste Start den Zustand tatsächlich gespeichert hat.
- Prüfen, ob beim Wechsel in den Hintergrund ein atomarer Speichervorgang abgeschlossen wurde.
- Sicherstellen, dass beim zweiten Start nicht erneut die Bereinigungslogik ausgeführt wurde.
- Prüfen, ob die Versionsmigration der persistenten Daten erfolgreich war.
- Abschließend prüfen, ob das Routing das wiederhergestellte Objekt akzeptiert.
Wenn der Entwurf wiederhergestellt wurde, die App aber zur Startseite zurückkehrt, liegt das Problem meist in der Rekonstruktion der Navigation. Ist die richtige Seite geöffnet, während die Felder leer bleiben, sollten der Persistenzzeitpunkt und die Speicherschlüssel geprüft werden. Schlagen ausschließlich parallele Jobs fehl, ist zuerst zu kontrollieren, ob Simulatoren, Ergebnisverzeichnisse und Fixtures tatsächlich isoliert sind.
Werden diese Szenarien als Merge-Gate ausgeführt, hängt die Zustandswiederherstellung nicht länger davon ab, dass jemand die App wiederholt manuell wechselt. Bei jeder Änderung am Persistenzmodell, am Scene-Lebenszyklus oder an der Navigationsstruktur kann die Pipeline mit denselben Eingaben prüfen, ob Benutzer ihre Arbeit weiterhin an der Unterbrechungsstelle fortsetzen können.
Häufig gestellte Fragen
Sollte der Test den täglich genutzten Entwickler-Simulator verwenden?
Nein. Eine eigene Simulatorinstanz oder ein separates Device Set verhindert, dass alte App-Daten den Test beeinflussen. Vor jedem unabhängigen Szenario sollte der definierte Ausgangszustand wiederhergestellt werden.
Warum reicht es nicht zu prüfen, ob die App erneut startet?
Ein erfolgreicher Start beweist nur, dass die App nicht sofort abstürzt. Zusätzlich müssen Entwurf, Navigationspfad, Auswahl und Wiederherstellungshinweis sowie das Entfernen sensibler temporärer Daten geprüft werden.
Den nächsten Auftrag auf einem Cloud-M4-Mac ausführen
Wählen Sie Runner M4 oder Runner M4 Plus und legen Sie Knoten, Laufzeit und zusätzliche Speicheroptionen passend zu Ihrem Projekt fest. Jede Bestellung umfasst einen dedizierten physischen Rechner, keine virtuelle Maschine.