Un utilisateur modifie un formulaire qui n’a pas encore été envoyé sur un Mac cloud, puis passe dans une autre application pour consulter des informations. Quelques minutes plus tard, le système met fin au processus. À la réouverture de l’application, le pire résultat n’est pas un plantage, mais une interface qui semble normale alors que le brouillon a disparu, que la navigation est revenue à l’accueil ou qu’un objet désormais invalide a été restauré. Ces scénarios sont difficiles à couvrir avec de simples tests unitaires. Il est plus pertinent de rejouer le cycle de vie complet avec XCUITest dans un simulateur isolé.
Commencer par définir les états à restaurer
Ne traitez pas la « restauration du dernier écran » comme une exigence unique. Avant d’écrire les tests, répartissez l’état en trois catégories :
| Type d’état | Exemples | Comportement attendu après la relance |
|---|---|---|
| État métier | Texte d’un brouillon, critères de filtrage, élément sélectionné | Restaurer selon les règles du produit |
| État de navigation | Écran actuel, onglet, position dans une liste | Restaurer jusqu’au niveau encore valide |
| État temporaire | Indicateur de chargement, boîte de dialogue, jeton à usage unique | Abandonner et recalculer |
La logique de restauration doit également distinguer un passage normal en arrière-plan d’un arrêt anormal. Dans le premier cas, davantage de détails d’interface peuvent être conservés. Dans le second, la cohérence des données doit être prioritaire. Si un brouillon dépend d’un objet qui a depuis été supprimé, l’application doit revenir vers un écran sûr et en expliquer la raison, plutôt que de recréer de force une interface devenue invalide.
L’objectif d’un test de restauration d’état n’est pas de remettre chaque pixel à sa place, mais de vérifier que l’utilisateur peut poursuivre son travail sans voir de données obsolètes, non autorisées ou impossibles à envoyer.
Pour chaque scénario, consignez quatre éléments : l’état initial, l’action d’arrêt, les assertions de restauration et les états interdits. En cas d’échec, l’équipe pourra ainsi déterminer si le problème vient de la persistance, du routage ou de la validation des données.
Préparer un point d’entrée déterministe pour l’automatisation
Les tests d’interface ne doivent pas dépendre des données laissées par une exécution précédente. Lorsqu’elle est lancée avec des arguments réservés aux tests, l’application peut importer un jeu de données fixe et l’écrire dans un conteneur distinct. Le nom de ce jeu de données doit décrire le scénario métier, plutôt que reprendre une clé primaire de base de données susceptible de changer.
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)
}
La build de production doit ignorer ou supprimer ce point d’entrée de test. Une solution plus robuste consiste à utiliser une compilation conditionnelle afin de limiter le chargeur aux configurations de test internes. Avant chaque test, supprimez également les notifications, les caches et les préférences partagées, afin qu’un ancien brouillon encore présent ne provoque pas un faux positif.
Même sur les nœuds physiques dédiés de Runner, chaque pipeline doit disposer de son propre ensemble d’appareils de simulation. Des ressources de calcul dédiées ne garantissent pas l’isolation des données de test : deux tâches parallèles utilisant le même simulateur peuvent toujours écraser leurs conteneurs d’application respectifs.
Rejouer l’arrêt et la relance du processus avec XCUITest
Le test ci-dessous commence par créer un brouillon, puis place l’application en arrière-plan, arrête son processus et la relance. Tous les contrôles importants doivent disposer d’un accessibility identifier stable, afin que la recherche des éléments ne dépende pas des libellés localisés.
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)
}
Ne transmettez pas à nouveau le paramètre de réinitialisation lors du second lancement au sein du même test. Le test supprimerait lui-même l’état qu’il doit valider. Ne vous contentez pas non plus de vérifier la présence de l’éditeur : contrôlez simultanément son contenu, le titre de navigation, l’objet sélectionné et l’indication confirmant la restauration.
Tester séparément les trois parcours de lancement
Conservez au moins trois cas de test indépendants :
- Retour direct de l’arrière-plan au premier plan, pour vérifier qu’une brève interruption ne recrée pas toute la page.
- Arrêt du processus après l’enregistrement de l’état, pour vérifier que le travail peut reprendre après la relance.
- Relance avec un objet devenu invalide, pour vérifier que l’application revient à un état sûr et supprime la route invalide.
Chaque cas ne doit valider qu’un seul cycle de vie. L’origine d’un échec sera ainsi plus facile à identifier que dans un long test comportant une dizaine d’étapes ou davantage.
Isoler le simulateur et les répertoires d’exécution sur un Mac cloud
La tâche d’automatisation doit créer explicitement un ensemble d’appareils temporaire, au lieu d’utiliser les simulateurs du répertoire par défaut de l’utilisateur connecté. Supprimer cet ensemble à la fin de la tâche permet de nettoyer à la fois les conteneurs des applications et les instantanés résiduels.
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"
La version d’exécution et le type d’appareil de cet exemple doivent être remplacés par ceux déjà installés sur le nœud. Exécutez d’abord xcrun simctl list runtimes et xcrun simctl list devicetypes pour les vérifier. N’autorisez pas le script à sélectionner silencieusement la version d’exécution « la plus récente » : après une mise à jour de Xcode, les captures d’écran, les boîtes de dialogue système et le comportement de restauration pourraient tous changer en même temps.
Transformer les preuves d’échec en résultats exploitables
Les échecs de restauration d’état se produisent généralement après le second lancement. Il faut donc conserver des éléments de preuve avant et après la restauration. Il est recommandé de joindre une capture d’écran après chaque action importante, puis d’ajouter aux XCTest attachment le nom du jeu de données utilisé, l’identifiant du simulateur, la version de l’application et l’étape de restauration. Conservez le fichier .xcresult en cas d’échec ; pour les tâches réussies, sa durée de conservation peut être réduite selon la politique de l’équipe.
L’ordre de diagnostic peut être standardisé comme suit :
- Vérifier que le premier lancement a bien enregistré l’état.
- Vérifier que l’enregistrement atomique s’est terminé lors du passage en arrière-plan.
- Confirmer que le second lancement n’a pas réexécuté la logique de nettoyage.
- Vérifier que la migration de version des données persistées a réussi.
- Vérifier enfin que le routage accepte l’objet restauré.
Si le brouillon est restauré, mais que l’application revient à l’accueil, le problème se situe probablement dans la reconstruction de la navigation. Si la bonne page s’affiche, mais que les champs sont vides, vérifiez le moment de la persistance et les clés de stockage. Si les échecs ne surviennent que lors des exécutions parallèles, vérifiez en priorité que les simulateurs, les répertoires de résultats et les jeux de données sont réellement isolés.
Une fois ces scénarios intégrés aux contrôles obligatoires avant fusion, la restauration d’état ne dépend plus de manipulations manuelles répétées entre les applications. À chaque modification du modèle de persistance, du cycle de vie Scene ou de la structure de navigation, le pipeline peut utiliser les mêmes données d’entrée pour vérifier que l’utilisateur est toujours en mesure de reprendre son travail là où il a été interrompu.
Questions fréquentes
Faut-il réutiliser le simulateur quotidien d’un développeur pour ces tests ?
Non. Utilisez un simulateur dédié ou un jeu d’appareils séparé, puis réinitialisez les données de l’application avant chaque scénario indépendant afin d’éviter les validations dues à un ancien état.
Pourquoi une simple vérification de la relance ne suffit-elle pas ?
La relance prouve seulement que l’application ne plante pas immédiatement. Il faut aussi vérifier le brouillon, le niveau de navigation, l’objet sélectionné, le message de reprise et l’absence de données temporaires sensibles.
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.