Nach einem Update der iOS-App können auf dem Gerät noch JSON-Caches liegen, die eine ältere Version geschrieben hat. Dass sich eine aktuelle Serverantwort problemlos decodieren lässt, heißt nicht, dass auch der alte Cache lesbar ist. Ein typischer Fehler: Ein Codable-Modell erhält ein neues Feld, die Tests decken aber nur neue Antworten ab. Erst wenn jemand eine Offline-Ansicht öffnet, tritt ein Decodierfehler auf. Die Lösung besteht nicht darin, jeden Fehler stillschweigend zu übergehen. Stattdessen werden echte historische Daten als feste Testdaten bewahrt und es wird festgelegt, welche Änderungen kompatibel sein müssen und welche einen Fehler auslösen müssen.
Kompatibilitätsgrenzen festlegen
Nehmen wir eine Kontoübersicht: id und status sind fachlich notwendige Felder, nickname kam später als optionales Feld hinzu. Erwartet wird, dass alte Daten ohne nickname lesbar bleiben, neue Daten mit dem Client noch unbekannten Schlüsseln ebenfalls lesbar sind und Daten ohne id fehlschlagen. Diese drei Eingaben müssen getrennt getestet werden. Eine einzige „normale Antwort“ belegt keine Kompatibilität.
In einer synthetisierten Decodable-Implementierung akzeptiert String? einen fehlenden Wert und liefert nil; fehlt ein nicht optionaler String, wird ein Fehler ausgelöst. Unbekannte Schlüssel werden beim synthetisierten Decodieren in der Regel ignoriert. Das betrifft jedoch nur die Decodierung: Wenn die Oberfläche nickname anschließend zwangsweise entpackt, kann ein alter Cache weiterhin einen Fehler verursachen. Nach erfolgreichen Modelltests muss deshalb auch der Code geprüft werden, der dieses Feld verwendet: Er braucht eine Ersatzanzeige.
Sowohl „alte Daten lesen“ als auch „beschädigte Daten zurückweisen“ gehören in die Tests. Wer nur Ersteres prüft, kaschiert leicht echte Datenfehler als Kompatibilitätsbehandlung.
Historische Antworten als feste Testdaten ablegen
Lege im Test-Target ein Verzeichnis Fixtures an und speichere darin account-v1.json, account-v2.json und account-missing-id.json. Die Dateinamen sollen die Form der Daten beschreiben, nicht das Datum eines CI-Laufs. Füge alle drei Dateien dem Test-Target unter Copy Bundle Resources hinzu. Dateien lediglich im Projektverzeichnis anzulegen, macht sie zur Testlaufzeit noch nicht verfügbar.
account-v1.json bewahrt eine alte Antwort mit id und status, aber ohne nickname. account-v2.json ergänzt nickname und einen Schlüssel, den das aktuelle Modell nicht deklariert. Im dritten Datensatz fehlt id; er dient als Gegenbeispiel, das fehlschlagen muss. Bereite die Testdaten aus anonymisierten echten Daten auf: Ersetze personenbezogene Angaben und Tokens, erhalte aber Feldtypen, fehlende Felder und Verschachtelungsebenen. Überschreibe nicht bei jeder API-Änderung die alten Dateien, sonst können die Tests Regressionen nicht mehr erkennen.
Ändert der Server ein Feld von einer Zeichenfolge in eine Zahl, speichere eine neue Version und kläre zuerst, ob die App beide Formate unterstützen muss. Versehe nicht einfach alle Felder mit einer großzügigen Zeichenfolgenkonvertierung, nur damit die Tests wieder grün werden. Ist Kompatibilität erforderlich, implementiere einen ausdrücklich dafür vorgesehenen eigenen Decodierpfad und füge passende Testdaten hinzu.
Drei Ergebnisse mit XCTest prüfen
Der folgende Test setzt voraus, dass die Testdaten in das Stammverzeichnis des Test-Bundles kopiert wurden. AccountSnapshot ist für dieses Beispiel separat deklariert. In einem echten Projekt sollte stattdessen das getestete Produktionsmodell verwendet werden, damit der Test nicht bloß eine provisorische Kopie bestätigt.
import Foundation
import XCTest
private struct AccountSnapshot: Decodable {
let id: String
let status: String
let nickname: String?
}
final class CodableContractTests: XCTestCase {
private func decode(_ name: String) throws -> AccountSnapshot {
let url = try XCTUnwrap(
Bundle(for: Self.self).url(
forResource: name,
withExtension: "json"
)
)
let data = try Data(contentsOf: url)
return try JSONDecoder().decode(AccountSnapshot.self, from: data)
}
func testOldResponseRemainsReadable() throws {
let account = try decode("account-v1")
XCTAssertEqual(account.id, "example-001")
XCTAssertNil(account.nickname)
}
func testUnknownFieldDoesNotBreakDecoding() throws {
let account = try decode("account-v2")
XCTAssertEqual(account.id, "example-001")
XCTAssertEqual(account.nickname, "Tester")
}
func testMissingRequiredFieldFails() {
XCTAssertThrowsError(try decode("account-missing-id"))
}
}
Verwende in beiden lesbaren Datensätzen dieselbe Beispiel-id, damit die Assertions eine eindeutige Aussage haben. Der Test für den Fehlerfall verlangt bisher nur, dass ein Fehler auftritt. Ist der Fehlertyp selbst Teil des Vertrags zur Datenverarbeitung, prüfe zusätzlich DecodingError.keyNotFound und den Feldnamen. So lässt sich die erwartete Ablehnung eines fehlenden Felds von einer nicht gefundenen Testressource unterscheiden.
Bei Datumswerten und Enums nicht auf Standardverhalten vertrauen
Enthalten die Testdaten Datumswerte, prüfe zuerst, welche JSONDecoder.dateDecodingStrategy der Produktionscode tatsächlich verwendet; der Test muss dieselbe Konfiguration nutzen. Wird ein Status mit festen Zeichenfolgen um einen neuen Enum-Fall erweitert, ist ebenfalls zu entscheiden, ob unbekannte Statuswerte erhalten bleiben und ein allgemeiner Text angezeigt wird oder ob die Verarbeitung abgelehnt wird. Halte diese Entscheidung im Modell und in den Testdaten fest, statt im Test eine eigene Decodierstrategie zu erfinden.
Tests in einem Cloud-Mac-Job ausführen
Prüfe zuerst mit xcodebuild -list das Projekt und das freigegebene Scheme. Ermittle dann die Geräte-ID aus der Liste der derzeit verfügbaren Simulatoren. Die Umgebungsvariablen im folgenden Befehl stellt dein CI-Job bereit; trage bei TEST_TARGET den Namen des Test-Targets ein. Eine Geräte-ID vermeidet Mehrdeutigkeiten bei gleichnamigen Simulatoren:
: "${PROJECT:?Set PROJECT}"
: "${SCHEME:?Set SCHEME}"
: "${SIM_ID:?Set SIM_ID}"
: "${TEST_TARGET:?Set TEST_TARGET}"
xcodebuild \
-project "$PROJECT" \
-scheme "$SCHEME" \
-destination "platform=iOS Simulator,id=$SIM_ID" \
"-only-testing:$TEST_TARGET/CodableContractTests" \
test
Prüfe auf einem CDNMac Cloud-Mac vor dem Start des Jobs, ob das gewählte Xcode das Projekt öffnen kann, das Scheme freigegeben ist und die Testressourcen im Bundle liegen. Hier laufen Datenvertragstests: Sie benötigen weder Signierungsdaten noch ein angeschlossenes physisches Gerät. CI muss den Exit-Status von xcodebuild übernehmen. Fehlende Testdaten, Decodierfehler oder nicht erfüllte Assertions müssen den Job fehlschlagen lassen; Erfolg darf nicht aus Schlüsselwörtern im Log abgeleitet werden.
Was bei der Pflege der Testdaten zu prüfen ist
Wenn eine Modelländerung ins Code-Review geht, frage Punkt für Punkt: Werden alte Caches noch gelesen? Was zeigt die Oberfläche an, wenn ein neu hinzugefügtes Feld in alten Daten fehlt? Sollen unbekannte Schlüssel ignoriert werden? Wohin gelangen Fehler bei beschädigten Pflichtfeldern? Prüfe anschließend, ob die Tests das Produktionsmodell und dessen Decodierkonfiguration verwenden, statt nur eine ähnlich aussehende Beispielimplementierung auszuführen.
Schlägt ein Test fehl, kläre zuerst die Fehlerquelle. Wird eine Datei nicht gefunden, prüfe ihre Zugehörigkeit zum Target und das Bundle. Bei keyNotFound ist zu klären, ob das Feld tatsächlich erforderlich oder eine neu hinzugefügte optionale Angabe ist. Bei typeMismatch prüfe, ob sich der Typ der echten Daten geändert hat. Schlägt eine Assertion fehl, untersuche die fachliche Bedeutung des decodierten Werts. Ersetze nicht für all diese Fälle die Fehlerbehandlung pauschal durch try? und ein leeres Objekt. Die bewahrten historischen Testdaten sind die ausführbare Dokumentation des Teams darüber, was die App weiterhin lesen kann.
Häufig gestellte Fragen
Sollten alte JSON-Testdaten nach einem neuen optionalen Feld bleiben?
Ja. Bestehende Caches enthalten das Feld nicht; der Test bestätigt, dass es als nil gelesen wird, ohne das gesamte Objekt abzulehnen.
Muss ein unbekannter JSON-Schlüssel bei Codable fehlschlagen?
Normalerweise nicht. Synthetisiertes Decodable ignoriert nicht deklarierte Schlüssel. Prüfe fehlende Pflichtfelder in einem eigenen Fehlertest.
Lassen Sie Ihren nächsten iOS-Build auf einem dedizierten Cloud-Mac laufen
Wählen Sie aus sechs Standorten und vier Mietlaufzeiten die passende Lösung für Ihre Aufgabe – mit M4, 16 GB RAM und 256 GB SSD auf einer festen physischen Maschine.