Article technique Runner

Créer des tests iOS reproductibles du presse-papiers avec simctl

Créer des tests iOS reproductibles du presse-papiers avec simctl

Lors de l’exécution à distance de tests iOS, les fonctions liées au presse-papiers tombent facilement dans un angle mort : elles fonctionnent d’un simple geste en local, mais restent impossibles à valider dans le pipeline. L’importation d’un code d’invitation, le collage de commandes de débogage, le nettoyage de texte multiligne et la copie depuis l’application dépendent tous du presse-papiers système, alors que les tests unitaires classiques contournent souvent les véritables points d’entrée. Une approche plus fiable consiste à attribuer un simulateur distinct à chaque tâche, à y injecter un texte fixe avec simctl, puis à laisser le test d’interface effectuer l’action de collage visible par l’utilisateur.

Commencer par définir le périmètre des tests

Ce processus convient à la validation du texte brut, d’Unicode, des espaces et des fins de ligne. Il ne faut pas considérer pbcopy comme un outil d’injection de données binaires arbitraires. Les images, les URL de fichiers et les UTI personnalisés doivent être fournis par des fixtures intégrées à l’application ou par un hôte de test dédié.

Commencez par recenser les comportements qui doivent réellement être protégés :

L’objectif de l’automatisation n’est pas de lire directement le presse-papiers système, mais de vérifier que l’application affiche le bon résultat après une action de copie ou de collage déclenchée par l’utilisateur.

Préparer un simulateur propre pour chaque tâche

Les tâches parallèles ne doivent pas partager la cible ambiguë booted. Si deux tâches exécutent pbcopy simultanément, le dernier contenu écrit remplace celui de la première. La couche d’orchestration doit fournir un UDID fixe et attribuer des répertoires distincts à DerivedData ainsi qu’aux bundles de résultats.

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

La commande erase supprime les conteneurs d’applications et les réglages du simulateur. Elle convient donc aux tâches de validation qui privilégient le déterminisme. Si le pipeline réutilise une application déjà installée afin de réduire le temps de préparation, le nettoyage doit néanmoins être prévu explicitement : désinstallez au minimum l’application testée, videz le presse-papiers et consignez la version du runtime ainsi que l’UDID.

Élément isolé Pratique recommandée Problème courant en cas de partage
Simulateur Un UDID par tâche Le presse-papiers et l’état de l’application s’écrasent mutuellement
DerivedData Un répertoire par tâche Les écritures concurrentes rendent les résultats de compilation instables
Bundle de résultats Un nouveau chemin à chaque exécution D’anciennes pièces jointes se retrouvent dans le rapport d’échec actuel
Fixtures texte Les versionner Les entrées changent sans modification du code de test

Injecter des fixtures texte auditables

Ne construisez pas directement des chaînes Unicode complexes dans le Shell. Stockez les fixtures dans des fichiers UTF-8 afin que les changements apportés aux entrées soient visibles pendant la revue de code.

mkdir -p Tests/ClipboardFixtures

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

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

Les fixtures doivent couvrir les entrées courantes comme les cas limites, sans pour autant regrouper tous les scénarios dans un unique fichier démesurément long. Il est préférable de maintenir des cas distincts pour une ligne, plusieurs lignes, les espaces de début et de fin, les caractères combinés et le contenu vide. En cas d’échec, ne consignez que le nom du fichier de fixture, le nombre d’octets et son empreinte. N’affichez pas systématiquement le contenu réel du presse-papiers, car il peut contenir des données sensibles.

Après l’injection, une lecture ponctuelle pendant le débogage permet de déterminer si le problème se situe du côté de l’injection ou de l’application :

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

Il n’est pas nécessaire de conserver durablement ce texte brut dans le pipeline de production. Après avoir confirmé qu’il correspond à la fixture, supprimez le fichier ou ne conservez que le résultat de shasum -a 256.

Déclencher le collage et la copie par les véritables points d’entrée

L’application doit proposer une commande de collage identifiable par l’utilisateur, plutôt que de lire discrètement le presse-papiers au chargement de l’écran. Le test d’interface active cette commande, puis vérifie le résultat affiché. Il couvre ainsi le parcours d’interaction sans confondre une lecture en arrière-plan réussie avec le bon fonctionnement de la fonctionnalité.

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:🧪"
    )
}

Si le système affiche une demande de confirmation du collage, le test doit la traiter comme une étape de l’interaction plutôt que la contourner au moyen d’une API interne. Les accessibility identifiers des contrôles doivent rester stables et ne pas dépendre du libellé localisé des boutons.

La copie peut être validée dans le sens inverse : le test d’interface saisit un contenu fixe et active la copie, puis le résultat est exporté avec pbpaste à la fin du test. Cette méthode permet de détecter les fins de ligne superflues, la conversion involontaire de texte enrichi en texte brut ou un nettoyage incorrect des espaces.

Exécuter les tests, recueillir les preuves et diagnostiquer les échecs

Terminez d’abord la compilation, puis exécutez les tests. Cela évite de mélanger les erreurs de compilation et les échecs d’interaction dans un même journal.

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"

En cas d’échec, vérifiez dans l’ordre que l’UDID appartient bien à la tâche en cours, que le démarrage du simulateur est terminé, que l’empreinte de la fixture correspond, que l’application a réellement activé la commande de collage et que l’assertion lit l’état final de l’interface. Ne relancez pas immédiatement le test après le premier échec : une nouvelle exécution peut masquer une condition de concurrence et faire disparaître les éléments utiles au diagnostic.

Vous pouvez prévoir un nettoyage à la sortie de la tâche tout en conservant, en cas d’échec, le fichier xcresult, les journaux du simulateur et les captures d’écran. Le contenu brut du presse-papiers doit être supprimé par défaut ; seuls sa longueur, son encodage, son empreinte et le nom de la fixture doivent subsister. Si le test porte sur des données potentiellement saisies par un utilisateur, les journaux doivent être expurgés avant leur archivage.

Pérenniser les contrôles dans le pipeline

Une tâche stable de non-régression du presse-papiers doit satisfaire au minimum les conditions suivantes :

  1. l’UDID, DerivedData et le répertoire de résultats sont isolés par tâche ;
  2. la version du runtime du simulateur est consignée ;
  3. les fixtures texte sont versionnées sous forme de fichiers UTF-8 ;
  4. le test déclenche le collage ou la copie par une action utilisateur ;
  5. le presse-papiers vide, Unicode et les fins de ligne disposent de cas distincts ;
  6. les preuves affichées par l’interface sont conservées en cas d’échec, sans stockage prolongé du contenu brut du presse-papiers ;
  7. l’état temporaire du simulateur est nettoyé à la fin de la tâche.

L’intérêt de ces tests n’est pas de couvrir le presse-papiers système lui-même, mais de transformer le contrat entre l’application et le presse-papiers en entrées et sorties reproductibles pour la validation. Lorsqu’un échec peut être associé à un UDID précis, à l’empreinte d’une fixture, aux étapes du test et à un bundle de résultats, le diagnostic à distance ne dépend plus d’un compte rendu manuel.

Questions fréquentes

simctl pbcopy suffit-il pour tester des images copiées ?

Non, ce n’est pas un injecteur binaire généraliste. Utilisez pbcopy et pbpaste pour les cas texte, puis un jeu de données intégré à l’application ou un hôte de test pour les images, URL de fichier et UTI personnalisés.

Plusieurs tâches peuvent-elles partager le même simulateur iOS ?

Ce n’est pas recommandé. Chaque tâche doit disposer de son UDID, de son répertoire DerivedData et de son dossier de résultats afin d’éviter tout écrasement du presse-papiers ou du conteneur applicatif.

Nœud physique dédié

Exécutez votre prochaine tâche sur un Mac M4 dans le cloud

Choisissez Runner M4 ou Runner M4 Plus, puis définissez le nœud, la durée de location et les options de stockage selon votre projet. Chaque commande comprend une machine physique dédiée, et non une machine virtuelle.

Louer un Mac dans le cloud