Runner-Engineering-Artikel

Reproduzierbare iOS-Zwischenablage-Tests auf einem Cloud-Mac

Reproduzierbare iOS-Zwischenablage-Tests auf einem Cloud-Mac

Bei der Remote-Ausführung von iOS-Tests geraten Funktionen rund um die Zwischenablage schnell in einen blinden Fleck: Lokal genügt ein Klick, doch in der Pipeline lässt sich das Verhalten nicht zuverlässig prüfen. Das Importieren von Einladungscodes, das Einfügen von Debug-Befehlen, die Bereinigung mehrzeiliger Texte und das Kopieren innerhalb der App hängen von der Systemzwischenablage ab. Herkömmliche Unit-Tests umgehen jedoch häufig den tatsächlichen Einstiegspunkt. Robuster ist es, jedem Auftrag einen eigenen Simulator zuzuweisen, mit simctl einen fest definierten Text einzuspielen und anschließend per UI-Test den für Benutzer sichtbaren Einfügevorgang auszuführen.

Zuerst den Testumfang festlegen

Dieser Ablauf eignet sich zum Prüfen von Klartext, Unicode, Leerraum und Zeilenumbrüchen. pbcopy sollte nicht als Werkzeug zum Einspeisen beliebiger Binärdaten verwendet werden. Bilder, Datei-URLs oder benutzerdefinierte UTIs sollten über Test-Fixtures innerhalb der App oder einen speziellen Test-Host bereitgestellt werden.

Zunächst sollten die tatsächlich abzusichernden Verhaltensweisen festgelegt werden:

Ziel der Automatisierung ist nicht, die Systemzwischenablage direkt auszulesen, sondern zu prüfen, ob die App nach einem vom Benutzer ausgelösten Kopier- oder Einfügevorgang das richtige Ergebnis anzeigt.

Für jeden Auftrag einen sauberen Simulator vorbereiten

Parallele Aufträge dürfen nicht dasselbe mehrdeutige Ziel booted verwenden. Wenn zwei Aufträge gleichzeitig pbcopy ausführen, überschreibt der zuletzt geschriebene Inhalt den vorherigen. Die Orchestrierungsschicht sollte eine feste UDID übergeben und für DerivedData sowie Ergebnispakete getrennte Verzeichnisse zuweisen.

set -euo pipefail

: "${IOS_UDID:?IOS_UDID is required}"
JOB_ROOT="${PWD}/artifacts/${CI_JOB_ID:-local}"
mkdir -p "$JOB_ROOT"

xcrun simctl shutdown "$IOS_UDID" 2>/dev/null || true
xcrun simctl erase "$IOS_UDID"
xcrun simctl boot "$IOS_UDID"
xcrun simctl bootstatus "$IOS_UDID" -b

erase entfernt App-Container und Simulator-Einstellungen und eignet sich daher für Abnahmetests, die deterministische Ergebnisse erfordern. Wenn die Pipeline installierte Apps wiederverwenden muss, um die Vorbereitungszeit zu verkürzen, darf das Bereinigungskonzept trotzdem nicht entfallen: Mindestens die zu testende App muss deinstalliert, die Zwischenablage geleert und die Runtime-Version zusammen mit der UDID protokolliert werden.

Isoliertes Objekt Empfohlenes Vorgehen Typisches Problem bei gemeinsamer Nutzung
Simulator Eine UDID pro Auftrag Zwischenablage und App-Zustand überschreiben sich gegenseitig
DerivedData Eigenes Verzeichnis pro Auftrag Gleichzeitige Schreibzugriffe führen zu instabilen Build-Ergebnissen
Ergebnispaket Für jeden Lauf einen neuen Pfad verwenden Alte Anhänge gelangen in den aktuellen Fehlerbericht
Text-Fixtures In die Versionsverwaltung aufnehmen Eingaben ändern sich, ohne dass sich der Testcode ändert

Nachvollziehbare Text-Fixtures einspeisen

Komplexe Unicode-Zeichenfolgen sollten nicht direkt in der Shell zusammengesetzt werden. Werden die Fixtures als UTF-8-Dateien gespeichert, ist bei der Codeprüfung sichtbar, wie sich die Eingabe verändert hat.

mkdir -p Tests/ClipboardFixtures

printf '第一行\n第二行:café\nemoji:🧪\n' \
  > Tests/ClipboardFixtures/multiline.txt

xcrun simctl pbcopy "$IOS_UDID" \
  < Tests/ClipboardFixtures/multiline.txt

Die Fixtures sollten normale Eingaben und Grenzfälle abdecken, aber nicht sämtliche Fälle in einer einzigen überlangen Datei bündeln. Empfehlenswert sind getrennte Testfälle für einzeilige und mehrzeilige Inhalte, führenden und nachgestellten Leerraum, kombinierende Zeichen sowie leere Inhalte. Bei einem Fehlschlag sollten die Protokolle nur den Namen der Fixture-Datei, ihre Byteanzahl und ihren Hash enthalten. Der tatsächliche Inhalt der Zwischenablage darf nicht bedingungslos ausgegeben werden, da er sensible Daten enthalten kann.

Während der Fehlersuche kann der Inhalt nach dem Einspeisen einmal ausgelesen werden, um festzustellen, ob das Problem auf der Eingabe- oder auf der App-Seite liegt:

xcrun simctl pbpaste "$IOS_UDID" > "$JOB_ROOT/clipboard-before.txt"
cmp Tests/ClipboardFixtures/multiline.txt \
  "$JOB_ROOT/clipboard-before.txt"

In einer produktiven Pipeline muss dieser Klartext nicht dauerhaft gespeichert werden. Nach erfolgreichem Vergleich sollte die Datei gelöscht oder ausschließlich das Ergebnis von shasum -a 256 aufbewahrt werden.

Einfügen und Kopieren über den echten Einstiegspunkt auslösen

Die App sollte ein für Benutzer erkennbares Steuerelement zum Einfügen anbieten, statt die Zwischenablage beim Laden der Seite unbemerkt auszulesen. Der UI-Test klickt auf dieses Steuerelement und prüft anschließend das Ergebnis in der Oberfläche. So wird der tatsächliche Interaktionspfad abgedeckt, ohne einen erfolgreichen Zugriff im Hintergrund fälschlich als funktionierendes Feature zu werten.

func testPasteMultilineFixture() {
    let app = XCUIApplication()
    app.launchArguments += ["-ui-testing"]
    app.launch()

    let pasteButton = app.buttons["clipboard.paste"]
    XCTAssertTrue(pasteButton.waitForExistence(timeout: 10))
    pasteButton.tap()

    let editor = app.textViews["clipboard.editor"]
    XCTAssertTrue(editor.waitForExistence(timeout: 5))
    XCTAssertEqual(
        editor.value as? String,
        "第一行\n第二行:café\nemoji:🧪"
    )
}

Wenn das System eine Bestätigung zum Einfügen anzeigt, muss der Test sie als Teil der Interaktion behandeln, statt sie über eine interne API zu umgehen. Die Accessibility Identifier der Steuerelemente müssen stabil sein und dürfen nicht von lokalisierten Schaltflächentexten abhängen.

Die Kopierrichtung lässt sich umgekehrt prüfen: Der UI-Test gibt einen fest definierten Inhalt ein und klickt auf „Kopieren“. Nach Testende wird das Ergebnis mit pbpaste exportiert. So lassen sich nachgestellte Zeilenumbrüche, die Herabstufung von Rich Text oder eine fehlerhafte Bereinigung von Leerraum erkennen.

Ausführen, Beweise sichern und Fehler eingrenzen

Zuerst sollte der Build abgeschlossen und erst danach der Test ausgeführt werden. Dadurch landen Kompilierungs- und Interaktionsfehler nicht vermischt im selben Protokoll.

xcodebuild build-for-testing \
  -scheme ClipboardApp \
  -destination "platform=iOS Simulator,id=$IOS_UDID" \
  -derivedDataPath "$JOB_ROOT/DerivedData"

xcodebuild test-without-building \
  -scheme ClipboardApp \
  -destination "platform=iOS Simulator,id=$IOS_UDID" \
  -derivedDataPath "$JOB_ROOT/DerivedData" \
  -resultBundlePath "$JOB_ROOT/ClipboardTests.xcresult"

Bei einem Fehlschlag sollte in dieser Reihenfolge geprüft werden: Gehört die UDID zum aktuellen Auftrag? Ist der Simulator vollständig gestartet? Stimmt der Hash des Fixtures überein? Hat die App das Einfüge-Steuerelement tatsächlich betätigt? Liest die Assertion den endgültigen Zustand der Oberfläche aus? Ein fehlgeschlagener Test sollte nicht sofort wiederholt werden. Ein erneuter Lauf kann Race Conditions verdecken und dadurch wichtige Diagnoseinformationen vernichten.

Für den Auftragsabschluss kann eine Bereinigung eingerichtet werden. Bei Fehlschlägen sollten jedoch xcresult, Simulator-Protokolle und Screenshots erhalten bleiben. Der Klartext aus der Zwischenablage sollte standardmäßig gelöscht werden; aufbewahrt werden nur Länge, Kodierung, Hash und Fixture-Name. Wenn der Test Inhalte umfasst, die von Benutzern eingegeben werden könnten, müssen die Protokolle vor der Archivierung anonymisiert werden.

Prüfpunkte dauerhaft in der Pipeline verankern

Ein stabiler Regressionstest für die Zwischenablage sollte mindestens die folgenden Bedingungen erfüllen:

  1. UDID, DerivedData und Ergebnisverzeichnis sind jeweils nach Auftrag isoliert;
  2. die Runtime-Version des Simulators wird protokolliert;
  3. Text-Fixtures werden als UTF-8-Dateien in die Versionsverwaltung aufgenommen;
  4. der Test löst Einfügen oder Kopieren durch eine Benutzeraktion aus;
  5. für eine leere Zwischenablage, Unicode und Zeilenumbrüche existieren getrennte Testfälle;
  6. bei Fehlschlägen werden Nachweise aus der Oberfläche gespeichert, der Klartext der Zwischenablage jedoch nicht dauerhaft aufbewahrt;
  7. nach Abschluss des Auftrags wird der temporäre Zustand des Simulators bereinigt.

Der Wert dieser Tests liegt nicht darin, die Systemzwischenablage selbst abzudecken. Entscheidend ist vielmehr, den Vertrag zwischen App und Zwischenablage in reproduzierbare Ein- und Ausgaben zu überführen. Wenn sich jeder Fehlschlag einer konkreten UDID, einem Fixture-Hash, einem Testschritt und einem Ergebnispaket zuordnen lässt, ist die Remote-Fehlersuche nicht länger auf manuelle Schilderungen angewiesen.

Häufig gestellte Fragen

Kann simctl pbcopy auch Bilder zuverlässig injizieren?

Nicht als allgemeiner Binärdaten-Injektor. Text lässt sich stabil mit pbcopy und pbpaste prüfen; Bilder, Datei-URLs und eigene UTI-Typen benötigen Fixtures in der App oder einen speziellen Test-Host.

Dürfen parallele Jobs denselben iOS-Simulator verwenden?

Nein, wenn reproduzierbare Ergebnisse erwartet werden. Jeder Job braucht eine eigene UDID sowie getrennte DerivedData- und Ergebnisverzeichnisse, damit sich Clipboard und App-Container nicht überschreiben.

Dedizierter physischer Knoten

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.

Cloud-Mac jetzt mieten