Engineering-Leitfaden

Reproduzierbare iOS-Simulatortests auf einem Cloud Mac

Reproduzierbare iOS-Simulatortests auf einem Cloud Mac

Dieselbe Suite von UI-Tests läuft lokal erfolgreich durch, verursacht auf einem entfernten Knoten jedoch sporadische Zeitüberschreitungen. Häufig liegt das nicht am Anwendungscode, sondern an verbliebenem Simulatorzustand, wechselnden Shard-Grenzen oder zu hoher Parallelität. Bei einer zuverlässigen Testmatrix müssen sich nach jedem Lauf drei Fragen beantworten lassen: Welche Runtime wurde verwendet, welche Tests wurden gemeinsam ausgeführt und wo befinden sich die gesicherten Fehlerdaten? Mit xcodebuild und simctl lässt sich dafür ein reproduzierbarer Ablauf aufbauen.

Zuerst die Grenzen der Testmatrix festlegen

Beginnen Sie nicht sofort mit sämtlichen Geräte- und Betriebssystemversionen. Wählen Sie zunächst eine primäre Runtime und ein repräsentatives Gerät, um eine stabile Basis herzustellen. Ergänzen Sie erst danach weitere Kombinationen für Kompatibilitätstests. Erfassen Sie vor jedem Lauf die Xcode-Version, die macOS-Version, die Runtime-Kennung, den Commit und die Testliste. Gerätenamen eignen sich nicht als eindeutige Schlüssel, da mehrere Simulatoren denselben Namen tragen können. Speichern Sie stattdessen die UDID.

Dimension Basiskonfiguration Bedingung für eine Erweiterung
System-Runtime Vom Projekt unterstützte Hauptversion Vor einem Release die niedrigste unterstützte Version abdecken
Gerätetyp Eine häufig verwendete Bildschirmgröße Bei Layoutanpassungen weitere Größen ergänzen
Test-Shards Feste Gruppierung nach Testklassen Erst aufteilen, wenn eine Gruppe zu lange läuft
Parallelität Mit zwei Shards beginnen Nach Stabilisierung der Ressourcenauslastung schrittweise erhöhen

Das Ziel einer Testmatrix besteht nicht darin, möglichst viele Kombinationen anzuhäufen. Derselbe Commit soll vielmehr bei identischen Eingaben nachvollziehbare Ergebnisse liefern.

Einmal verwendete, saubere Simulatoren erstellen

Fragen Sie zunächst ab, welche Runtimes und Gerätetypen auf dem Knoten tatsächlich vorhanden sind. Versionsnummern sollten nicht fest im Skript hinterlegt werden.

xcrun simctl list runtimes
xcrun simctl list devicetypes
xcrun simctl list devices available

Wählen Sie aus der Ausgabe die passenden Kennungen aus und erstellen Sie für jeden Shard ein separates Gerät. Der Name dient nur der besseren Lesbarkeit; alle weiteren Befehle verwenden die UDID.

RUNTIME_ID="com.apple.CoreSimulator.SimRuntime.iOS-XX-X"
DEVICE_TYPE_ID="com.apple.CoreSimulator.SimDeviceType.iPhone-XX"

UDID=$(xcrun simctl create "ui-shard-01" "$DEVICE_TYPE_ID" "$RUNTIME_ID")
xcrun simctl boot "$UDID"
xcrun simctl bootstatus "$UDID" -b

Verwenden Sie keine Simulatoren, die Entwickler regelmäßig zum Debuggen einsetzen. Tests können Berechtigungen, Regionseinstellungen, Tastaturen, App-Daten und Hintergrundaufgaben verändern. Kurzlebige Geräte begrenzen diese Zustandsänderungen auf einen einzelnen Lauf. Benötigen Tests Zugriff auf Kamera, Benachrichtigungen oder Standortdaten, müssen die entsprechenden Berechtigungen in der Vorbereitungsphase ausdrücklich gesetzt werden. Verlassen Sie sich nicht darauf, dass eine frühere Freigabe noch vorhanden ist.

Einmal kompilieren und anschließend nach festen Listen aufteilen

Die Matrix sollte nicht für jeden Shard erneut kompilieren. Führen Sie zuerst build-for-testing aus, um ein gemeinsames Testartefakt zu erzeugen. Die einzelnen Shards starten danach test-without-building.

DERIVED_PATH="$PWD/.derived-test"

xcodebuild build-for-testing \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination "generic/platform=iOS Simulator" \
  -derivedDataPath "$DERIVED_PATH"

XCTESTRUN=$(find "$DERIVED_PATH" -name "*.xctestrun" -print -quit)

Die Listen zur Aufteilung der Shards sollten im Repository versioniert werden. So verhindern Sie, dass sich die Grenzen bei jedem Lauf durch eine spontane Sortierung nach Alphabet oder Laufzeit verschieben. Beispielsweise können An- und Abmeldetests gemeinsam in einem Shard liegen, während Offline- und Synchronisierungstests einen anderen bilden. Deaktivieren Sie bei der Ausführung die interne Parallelisierung von Xcode, damit ausschließlich der äußere Scheduler die Parallelität steuert.

xcodebuild test-without-building \
  -xctestrun "$XCTESTRUN" \
  -destination "platform=iOS Simulator,id=$UDID" \
  -only-testing:"AppUITests/AuthenticationTests" \
  -parallel-testing-enabled NO \
  -resultBundlePath "$PWD/results/shard-01.xcresult"

Unterscheiden sich die Laufzeiten der Shards erheblich, passen Sie die Listen anhand der Medianlaufzeiten mehrerer aktueller Durchläufe an. Die neue Aufteilung muss anschließend fest versioniert und die Änderung dokumentiert werden. Der Scheduler darf die Tests nicht vor jedem Lauf automatisch neu verteilen, da sich Fehler sonst nicht direkt mit dem vorherigen Durchlauf vergleichen lassen.

Parallelität begrenzen und Simulatoren zuverlässig bereinigen

Mehr parallele Simulatoren bedeuten nicht automatisch kürzere Laufzeiten. Jeder zusätzliche Shard erzeugt weitere Startprozesse, Grafikdienste, Testprozesse und App-Instanzen. Beginnen Sie mit zwei Shards und beobachten Sie Speicherdruck, CPU-Auslastung, Startdauer und Gesamtlaufzeit. Reduzieren Sie die Parallelität sofort, wenn Swap-Nutzung auftritt, bootstatus mit einem Timeout endet oder die Laufzeiten einzelner Tests stark schwanken.

Das Skript muss die Bereinigung nach erfolgreichen, fehlgeschlagenen und abgebrochenen Läufen ausführen. Registrieren Sie für die erstellten UDIDs einen Exit-Handler, der die Geräte zuerst herunterfährt und anschließend löscht. Dadurch kann der nächste Lauf nicht versehentlich einen alten Simulator verwenden.

cleanup() {
  xcrun simctl shutdown "$UDID" >/dev/null 2>&1 || true
  xcrun simctl delete "$UDID" >/dev/null 2>&1 || true
}

trap cleanup EXIT INT TERM

Löschen Sie nicht mit einem globalen Befehl sämtliche Simulatoren des Knotens, da dort gleichzeitig andere Aufgaben laufen können. Verarbeiten Sie ausschließlich die UDIDs, die im aktuellen Lauf erstellt und protokolliert wurden. Soll der Zustand eines fehlgeschlagenen Tests erhalten bleiben, archivieren Sie zunächst die Protokolle und Ergebnispakete, bevor Sie das Gerät löschen.

Echte Regressionen und sporadische Fehler anhand der Ergebnispakete unterscheiden

Jeder Shard benötigt einen eigenen .xcresult-Pfad. Speichern Sie zusätzlich die Standardausgabe, Start- und Endzeit, den Exit-Code sowie die UDID. Wird derselbe Pfad mehrfach verwendet, können die besonders wertvollen Daten des ersten Fehlers überschrieben werden. Wiederholen Sie nach einem Fehlschlag nur den betroffenen Shard genau einmal, anstatt sofort die gesamte Matrix erneut auszuführen.

Schlägt der erste Lauf fehl und der zweite ist erfolgreich, deutet das meist auf instabile Wartebedingungen, Animationen, Netzwerkabhängigkeiten oder konkurrierende Zustandsänderungen hin. Scheitern beide Läufe an derselben Stelle, ist eine deterministische Regression wahrscheinlicher. Treten die beiden Fehler an unterschiedlichen Stellen auf, sollten Sie zuerst die Ressourcen des Knotens und gemeinsam genutzte Zustände zwischen den Tests untersuchen.

Prüfen Sie vor der Bereitstellung die folgenden Punkte in dieser Reihenfolge:

  1. Runtime und Gerätetyp stammen aus den Abfrageergebnissen des aktuellen Knotens.
  2. Jeder Shard verwendet eine eigene UDID und ein separates Ergebnisverzeichnis.
  3. Die Testartefakte werden nur einmal erstellt; die Shards kompilieren nicht erneut.
  4. Die Testlisten sind versioniert und werden vor einem Lauf nicht zufällig neu angeordnet.
  5. Die Ergebnisse des ersten Fehlers werden durch die Wiederholung nicht überschrieben.
  6. Nach einem Abbruch werden nur die Simulatoren gelöscht, die für den aktuellen Lauf erstellt wurden.

Häufig gestellte Fragen

Wie viele iOS-Simulatoren sollten auf einem Cloud Mac parallel laufen?

Beginnen Sie mit zwei Shards und erhöhen Sie erst nach Prüfung von Speicherdruck, CPU-Auslastung, Startzeit und Laufzeitschwankungen. Bei Auslagerung oder Timeouts muss die Parallelität sinken.

Warum benötigt jeder Test-Shard einen eigenen Simulator?

Eigene Simulatoren trennen App-Daten, Berechtigungen und Hintergrundprozesse. Dadurch beeinflussen sich Shards nicht gegenseitig und lassen sich vollständig neu erstellen.

Soll nach einem Fehler die gesamte Testsuite erneut laufen?

Nein. Sichern Sie zuerst das ursprüngliche xcresult-Paket und wiederholen Sie nur den fehlgeschlagenen Shard einmal. Ein anschließender Erfolg weist auf einen möglicherweise instabilen Test hin.

ArmMacs Cloud Mac

Dedizierte physische Knoten projektbezogen nutzen

Wählen Sie Chip, Arbeitsspeicher, Speicher, Mietdauer und verfügbare Knoten fest aus. Sobald Bestand verfügbar ist, beginnt der Bereitstellungsprozess.

Modell auswählen und bestellen