Regressionstests für iOS-Datenbankmigrationen aufbauen

Regressionstests für iOS-Datenbankmigrationen aufbauen

Eine scheinbar harmlose Änderung an einem Core-Data-Feld funktioniert bei einer Neuinstallation im Simulator meist problemlos, kann Benutzer beim direkten Upgrade von einer älteren Version jedoch auf dem Startbildschirm festhalten. In die CI gehört daher nicht nur die Prüfung, ob sich eine leere Datenbank anlegen lässt. Entscheidend ist, ob eine alte Datenbank über einen unterstützten Pfad migriert werden kann, ob zentrale Datensätze konsistent bleiben und ob ein Fehler genügend Diagnoseinformationen hinterlässt. Ein dauerhaft verfügbarer Cloud-Mac eignet sich gut, um diese zeitaufwendigen, aber deterministischen Prüfungen als eigenständige Freigabestufe auszuführen.

Zuerst den unterstützten Migrationsumfang festlegen

Listen Sie zunächst alle Datenbankversionen auf, die bei Benutzern noch vorkommen können. Gehen Sie nicht davon aus, dass jede Version schrittweise aktualisiert wird. Wenn das aktuelle Modell V5 ist, während noch Clients mit V3 und V4 im Einsatz sind, müssen mindestens die Pfade V3→V5 und V4→V5 getestet werden. Ein Test mit einer leeren V5-Datenbank belegt lediglich, dass das aktuelle Modell geladen werden kann. Umbenannte alte Felder, geänderte Beziehungseinschränkungen oder Konflikte bei eindeutigen Indizes werden dadurch nicht abgedeckt.

Dafür empfiehlt sich eine einfache Matrix:

Fixture Upgrade-Ziel Pflichtprüfungen
V3-Basisdatenbank V5 Anzahl der Konten, Projekte und historischen Aufgaben
V4-Grenzfalldatenbank V5 Leere Beziehungen, doppelte Namen, Löschmarkierungen
Großer V4-Datenbestand V5 Migrationsdauer und maximale Dateigröße
Leere V5-Datenbank V5 Initialisierung des aktuellen Modells

Eine Migration sollte die Freigabestufe nur bestehen, wenn die Datensemantik erhalten bleibt – nicht bereits dann, wenn der Persistenzcontainer keinen Fehler auslöst.

Halten Sie für jedes Fixture außerdem fest, mit welcher App-Version es erstellt wurde, welche Modellkennung gilt, wie viele Datensätze erwartet werden und welche Prüfzusammenfassung dazugehört. Verwenden Sie keine spontanen Kopien aus einer Datenbank, mit der Entwickler im Alltag arbeiten. Deren Inhalt verändert sich laufend, sodass Fehler später nur schwer reproduzierbar sind.

Reproduzierbare Fixtures alter Datenbanken einfrieren

Erstellen Sie für jede historische Version einen speziellen Build, der einen fest definierten Datenbestand schreibt. Dieser sollte reguläre Datensätze, leere Werte, lange Texte, archivierte Objekte und Objekte nahe an den Grenzen von Einschränkungen enthalten. Beenden Sie den Persistenzcontainer nach dem Schreiben ordnungsgemäß und kopieren Sie die Datenbank erst danach.

Bei SQLite mit WAL können die Daten auf drei Dateien verteilt sein. Am zuverlässigsten ist es, die App alle Änderungen speichern und anschließend einen Checkpoint ausführen zu lassen. Wenn sich das nicht sicherstellen lässt, muss die gesamte Dateigruppe archiviert werden:

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"

Öffnen Sie im Test niemals direkt das Original-Fixture aus dem Repository. Jeder Test sollte es zunächst in ein temporäres Verzeichnis kopieren und die Kopie nach der Migration löschen. So können parallele Jobs die Daten nicht gegenseitig verändern. Enthält ein Fixture echte Benutzerinhalte, Token oder Verbindungsdaten, erstellen Sie stattdessen einen neuen anonymisierten Datenbestand. Verlassen Sie sich nicht auf eine nachträgliche Bereinigung.

Die Migration über einen testbaren Einstiegspunkt kapseln

Beim App-Start sind Datenbankzugriff, Initialisierung der Benutzeroberfläche und Netzwerkanfragen häufig eng miteinander verknüpft. Dadurch lassen sich Migrationsfehler nur schwer eingrenzen. Kapseln Sie die Erstellung des Persistenzcontainers deshalb in einer Komponente, der sich eine URL übergeben lässt. XCTest kann dann direkt eine temporäre Kopie laden.

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
}

Eine Lightweight Migration eignet sich für neue optionale Felder, Attribute mit Standardwerten und eindeutig deklarierte Umbenennungen. Werden Entitäten aufgeteilt, Daten zusammengeführt oder Werte umgewandelt, ist ein explizites Mapping oder eine mehrstufige Migration erforderlich. Löschen Sie keine alten Modelle, nur damit die Tests bestehen. Die historischen Modelle werden benötigt, um Metadaten alter Datenbanken zu lesen und den Migrationspfad abzuleiten.

Fachliche Invarianten prüfen

Prüfen Sie nach der Migration mindestens vier Ebenen: Der Persistenzspeicher wurde erfolgreich geladen, die Modellversion wurde aktualisiert, die Anzahl der Datensätze entspricht den Erwartungen und die Semantik wichtiger Beziehungen und Felder ist weiterhin korrekt. So darf sich etwa die Gesamtzahl der Aufgaben nicht ändern, archivierte Projekte müssen archiviert bleiben und die Anzahl verwaister untergeordneter Objekte muss null betragen.

Verlassen Sie sich beim Vergleich nicht ausschließlich auf automatisch erzeugte Objekt-IDs. Schreiben Sie stabile fachliche Schlüssel in die Testdaten und prüfen Sie die Felder anhand dieser Schlüssel. Sortierungen und mengenartige Beziehungen sollten vor dem Vergleich normalisiert werden, damit bedeutungslose Reihenfolgeunterschiede keine sporadischen Fehler verursachen.

Einen separaten Testjob auf dem Cloud-Mac ausführen

Migrationstests sollten getrennt von regulären Unit-Tests laufen, da sie Fixtures kopieren und wiederholt Persistenzspeicher anlegen. Legen Sie die Simulator-Runtime und den Namen des Zielgeräts fest, starten Sie zunächst das Gerät und führen Sie anschließend den vorgesehenen Testplan aus:

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

Bereinigen Sie vor dem Start des Testjobs das temporäre Verzeichnis. Archivieren Sie nach dem Lauf unabhängig vom Ergebnis xcresult, Migrationsprotokolle, die Fixture-Zusammenfassung und die Kennung des Zielmodells. Laden Sie die vollständig migrierte Datenbank nicht standardmäßig als Artefakt hoch. Sie kann Testgeheimnisse enthalten und den Speicherbedarf schnell erhöhen. In der Regel sollte eine anonymisierte Fixture-Kopie nur bei einem Fehler gespeichert und nach einem klar definierten Zeitraum gelöscht werden.

Binden Sie den Job auf ZoomMini an eine feste Xcode-Toolchain und prüfen Sie in der Konsole, welche Konfigurationen aktuell verfügbar sind. Nach einem Wechsel von Xcode oder der Simulator-Runtime sollte zunächst nur der Migrationsplan ausgeführt werden. Nehmen Sie ihn erst danach wieder in die vollständige Pipeline auf. So gelangen Änderungen an Toolchain und Modell nicht gleichzeitig in den Prozess und erschweren die Ursachenanalyse.

Häufige Fehler und Prüfliste vor dem Release

Wenn ein Test lokal besteht, aber in der CI fehlschlägt, liegt das meist an einer unvollständig kopierten Dateigruppe, einem durch den vorherigen Test veränderten Fixture, fehlenden Modellressourcen im Test-Bundle oder mehreren Tests, die gleichzeitig denselben Pfad verwenden. Prüfen Sie zuerst SHA-256, kontrollieren Sie anschließend, ob das temporäre Verzeichnis eindeutig ist, und stellen Sie zuletzt sicher, dass das Build-Produkt alle unterstützten Modellversionen enthält.

Prüfen Sie vor dem Release jeden der folgenden Punkte:

  • Für jede unterstützte Ausgangsversion gibt es ein festes Fixture und eine erwartete Zusammenfassung.
  • Die SQLite-Hauptdatei und der WAL-Zustand sind konsistent.
  • Tests migrieren ausschließlich temporäre Kopien und verändern nicht das ursprüngliche Fixture.
  • Die Einsatzgrenzen von Lightweight Migration und explizitem Mapping sind dokumentiert.
  • Für Datensatzanzahl, stabile fachliche Schlüssel, Beziehungen und Standardwerte gibt es Assertions.
  • Fehlgeschlagene Jobs bewahren xcresult und anonymisierte Migrationsprotokolle auf.
  • Das Löschen einer Modellversion, das Umbenennen eines Feldes und Änderungen an Einschränkungen müssen den Migrationsplan auslösen.
  • Der vollständige Test aktualisiert direkt von der ältesten unterstützten Version auf die aktuelle Version.

Eine Datenbankmigration ist kein einmaliges Release-Skript, sondern ein Kompatibilitätsvertrag, der mit dem Modell weiterwächst. Sind historische Fixtures, fachliche Invarianten und Fehlernachweise fest definiert, lässt sich vor jeder Zusammenführung dieselbe Frage beantworten: Können vorhandene Daten die neue Version weiterhin sicher erreichen?

Häufig gestellte Fragen

Welche historischen Datenbankversionen gehören in den Testbestand?

Mindestens die aktuelle Produktivversion und die weiterhin direkt aktualisierbare Vorgängerversion. Bei übersprungenen Releases müssen alle unterstützten Ausgangsversionen enthalten sein.

Warum reicht die zentrale SQLite-Datei nicht aus?

Im WAL-Modus können bestätigte Datensätze noch in der Datei -wal liegen. Ohne sicheren Checkpoint müssen sqlite, sqlite-wal und sqlite-shm gemeinsam archiviert werden.

ZoomMini Cloud-Mac

Wählen Sie dedizierte physische Knoten für Builds, Tests und Experimente

Zwei M4-Konfigurationen sind an Knotenstandorten in Singapur, Tokio, Seoul und Hongkong verfügbar. Maßgeblich ist der bei der Bestellung angezeigte aktuelle Verfügbarkeitsstatus.

Modell auswählen und bestellen