Une modification apparemment anodine d’un champ Core Data peut parfaitement fonctionner sur un simulateur fraîchement installé, tout en bloquant sur l’écran de démarrage les utilisateurs qui effectuent une mise à niveau directe depuis une ancienne version. Le véritable contrôle à intégrer à la CI ne consiste pas à vérifier qu’une base vide peut être créée, mais à s’assurer qu’une ancienne base peut suivre un chemin de migration pris en charge, que les enregistrements critiques restent cohérents et qu’un échec laisse suffisamment d’éléments pour établir un diagnostic. Un Mac cloud disponible en continu convient particulièrement bien à ce type de contrôle autonome, long mais déterministe.
Définir d’abord le périmètre des migrations prises en charge
Commencez par recenser toutes les versions de base de données encore susceptibles d’exister, sans supposer que les utilisateurs installent chaque mise à jour intermédiaire. Si le modèle actuel est V5 alors que des clients V3 et V4 sont toujours en circulation, testez au minimum les migrations V3→V5 et V4→V5. Le test d’une base V5 vide prouve uniquement que le modèle actuel peut être chargé. Il ne couvre ni le renommage d’anciens champs, ni l’évolution des contraintes de relation, ni les conflits d’index uniques.
Il est recommandé de tenir à jour une matrice simple :
| Échantillon | Cible de mise à niveau | Contrôles obligatoires |
|---|---|---|
| Base V3 de référence | V5 | Nombre de comptes, de projets et de tâches historiques |
| Base V4 avec cas limites | V5 | Relations vides, noms en double, indicateurs de suppression |
| Grand échantillon V4 | V5 | Durée de la migration et taille maximale des fichiers |
| Base V5 vide | V5 | Initialisation du modèle actuel |
Le critère de réussite du contrôle de migration doit être la préservation de la sémantique des données, et non la simple absence d’erreur lors du chargement du conteneur persistant.
Pour chaque échantillon, consignez également la version de l’application qui l’a généré, l’identifiant du modèle, le nombre d’enregistrements attendu et le résumé de contrôle. N’utilisez pas comme échantillon ponctuel une base manipulée quotidiennement par un développeur : son contenu évoluerait au fil des opérations et les échecs deviendraient difficiles à reproduire.
Figer des échantillons reproductibles d’anciennes bases
Préparez une build dédiée pour chaque version historique et utilisez-la pour écrire un jeu de données fixe. Celui-ci doit inclure des enregistrements ordinaires, des valeurs nulles, des textes longs, des objets archivés et des objets proches des limites imposées par les contraintes. Une fois l’écriture terminée, fermez proprement le conteneur persistant avant de copier la base de données.
Lorsque SQLite utilise le mode WAL, les données peuvent être réparties entre trois fichiers. La méthode la plus fiable consiste à laisser l’application terminer l’enregistrement puis exécuter un checkpoint. Si cela ne peut pas être garanti, archivez ensemble tous les fichiers concernés :
set -euo pipefail
STORE="$HOME/Library/Developer/CoreSimulator/Devices/$SIM_UDID/data/Containers/Data/Application/$APP_ID/Library/Application Support/App.sqlite"
OUT="MigrationFixtures/V4"
mkdir -p "$OUT"
cp "$STORE" "$OUT/App.sqlite"
for suffix in -wal -shm; do
if [ -f "${STORE}${suffix}" ]; then
cp "${STORE}${suffix}" "$OUT/App.sqlite${suffix}"
fi
done
find "$OUT" -type f -print0 | sort -z | xargs -0 shasum -a 256 > "$OUT/SHA256SUMS"
N’ouvrez jamais directement, pendant les tests, l’échantillon d’origine conservé dans le dépôt. Chaque test doit d’abord le copier dans un répertoire temporaire, puis supprimer cette copie après la migration afin d’éviter que des tâches parallèles ne modifient les mêmes fichiers. Si les échantillons contiennent des données utilisateur réelles, des jetons ou des informations de connexion, recréez-les avec des données anonymes au lieu de compter sur une anonymisation ultérieure.
Encapsuler la migration dans un point d’entrée testable
Le démarrage d’une application associe souvent le chargement de la base, l’initialisation de l’interface et les requêtes réseau, ce qui complique l’identification d’un échec de migration. Encapsulez la création du conteneur persistant dans un composant acceptant une URL injectée, afin que XCTest puisse charger directement une copie temporaire.
func makeContainer(storeURL: URL) throws -> NSPersistentContainer {
let container = NSPersistentContainer(name: "AppModel")
let description = NSPersistentStoreDescription(url: storeURL)
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
container.persistentStoreDescriptions = [description]
var loadError: Error?
container.loadPersistentStores { _, error in
loadError = error
}
if let loadError {
throw loadError
}
return container
}
La migration légère convient à l’ajout de champs facultatifs, à l’ajout de propriétés dotées d’une valeur par défaut et aux renommages explicitement déclarés. Lorsqu’une évolution implique la scission d’entités, la fusion de données ou la conversion de valeurs, fournissez un mapping explicite ou procédez par étapes. Ne supprimez pas les anciens modèles dans le seul but de faire passer les tests : les modèles historiques sont indispensables pour lire les métadonnées des anciennes bases et déterminer le chemin de migration.
Vérifier les invariants métier
Après la migration, vérifiez au minimum quatre niveaux : le chargement réussi du stockage persistant, la mise à jour de la version du modèle, la conformité du nombre d’enregistrements et la validité sémantique des relations et champs essentiels. Par exemple, le nombre total de tâches ne doit pas changer, les projets archivés doivent le rester et le nombre d’objets enfants orphelins doit être nul.
Évitez de baser les comparaisons uniquement sur des identifiants d’objet générés automatiquement. Attribuez plutôt des clés métier stables aux données de test, puis contrôlez les champs à partir de ces clés. Pour les relations ordonnées ou les collections, normalisez les données avant de les comparer afin que des différences d’ordre sans incidence ne provoquent pas d’échecs intermittents.
Exécuter une tâche de test dédiée sur un Mac cloud
Les tests de migration doivent être exécutés séparément des tests unitaires ordinaires, car ils nécessitent de copier des échantillons et de créer fréquemment des stockages persistants. Fixez la version d’exécution du simulateur et le nom de l’appareil cible, démarrez d’abord l’appareil, puis exécutez le plan de test prévu :
set -euo pipefail
xcrun simctl bootstatus "$SIM_UDID" -b
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-testPlan DatabaseMigration \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
-resultBundlePath Artifacts/MigrationTests.xcresult \
CODE_SIGNING_ALLOWED=NO
Nettoyez les répertoires temporaires avant le démarrage de la tâche. À la fin, qu’elle réussisse ou échoue, archivez le fichier xcresult, les journaux de migration, le résumé des échantillons et l’identifiant du modèle cible. Ne publiez pas par défaut la base complète après migration comme artefact : elle peut contenir des secrets de test et accroître rapidement l’espace de stockage utilisé. En règle générale, ne conservez une copie anonymisée de l’échantillon qu’en cas d’échec, avec une politique de suppression clairement définie.
Sur ZoomMini, associez la tâche à une chaîne d’outils Xcode déterminée et vérifiez dans la console les configurations actuellement disponibles. Après tout changement de version de Xcode ou d’environnement d’exécution du simulateur, lancez d’abord le plan de migration seul avant de réactiver le pipeline complet. Vous éviterez ainsi d’introduire simultanément une évolution de la chaîne d’outils et une évolution du modèle, ce qui compliquerait l’identification de la cause d’un échec.
Échecs fréquents et contrôles avant publication
Un test qui réussit en local mais échoue dans la CI est généralement dû à une copie incomplète du groupe de fichiers, à un échantillon modifié par le test précédent, à l’absence des ressources de modèle dans le bundle de test ou à plusieurs tests manipulant simultanément le même chemin. Vérifiez d’abord les sommes SHA-256, assurez-vous ensuite que chaque répertoire temporaire est unique, puis confirmez que le produit compilé contient toutes les versions de modèle prises en charge.
Avant la publication, contrôlez chaque point :
- chaque version de départ prise en charge dispose d’un échantillon figé et d’un résumé attendu ;
- le fichier SQLite principal et l’état du WAL sont cohérents ;
- les tests migrent uniquement des copies temporaires et ne modifient pas la fixture d’origine ;
- les limites d’utilisation de la migration légère et des mappings explicites sont documentées ;
- le nombre d’enregistrements, les clés métier stables, les relations et les valeurs par défaut font tous l’objet d’assertions ;
- toute tâche en échec conserve le fichier
xcresultet les journaux de migration anonymisés ; - la suppression d’une version de modèle, le renommage d’un champ ou la modification d’une contrainte doit déclencher le plan de migration ;
- le test complet effectue une mise à niveau directe de la plus ancienne version prise en charge vers la version actuelle.
La migration d’une base de données n’est pas un simple script de publication ponctuel, mais un contrat de compatibilité qui s’enrichit au fil de l’évolution du modèle. Une fois les échantillons historiques, les invariants métier et les éléments de diagnostic figés, chaque modification du modèle permet de répondre avant la fusion à la même question : les données existantes peuvent-elles encore atteindre la nouvelle version sans risque ?
Questions fréquentes
Quelles versions historiques faut-il conserver pour les tests ?
Conservez au minimum la version actuellement en production et la précédente encore susceptible d’être mise à niveau. Ajoutez chaque point de départ direct toujours pris en charge.
Pourquoi copier aussi les fichiers WAL et SHM ?
Avec le mode WAL, des transactions validées peuvent rester dans le fichier -wal. Copier uniquement le fichier SQLite principal peut donc produire une fixture incomplète ou incohérente.
Choisissez un nœud physique dédié pour compiler, tester et expérimenter
Deux configurations M4 sont disponibles sur des nœuds à Singapour, Tokyo, Séoul et Hong Kong. La disponibilité réelle est confirmée par le résultat retourné lors de la commande.