Sobald Remote-Builds von echten Test-APIs abhängen, kann derselbe Commit morgens erfolgreich durchlaufen und nachmittags fehlschlagen: Testdaten wurden überschrieben, Tokens sind abgelaufen, das Gateway begrenzt die Anfragerate oder die API reagiert vorübergehend langsamer. Noch problematischer ist, dass am fehlgeschlagenen Build häufig nur ein Decodierungsfehler zu sehen ist. Was der Client tatsächlich empfangen hat, lässt sich dann nicht mehr nachvollziehen. Die Lösung besteht nicht in zusätzlichen Wiederholungsversuchen, sondern in zwei getrennten Prüfpfaden für die Netzwerkschicht: Der Großteil der Regressionstests verwendet lokale, unveränderliche Antworten; nur wenige Vertragstests greifen auf die echte Umgebung zu.
Grenzen von Offline-Tests klar definieren
URLProtocol ist Teil der Verarbeitungskette von URLSession und kann eine benutzerdefinierte Antwort zurückgeben, bevor eine Anfrage tatsächlich gesendet wird. Damit lassen sich vier Punkte prüfen: ob HTTP-Methode und Pfad stimmen, ob Header und Request-Body vollständig sind, ob das Modell die Antwort decodieren kann und ob die Geschäftslogik Statuscodes eindeutigen Fehlern zuordnet.
URLProtocol kann weder belegen, dass der echte Dienst aktuell erreichbar ist, noch Änderungen an der Gateway-Konfiguration erkennen. Deshalb empfiehlt sich eine Aufteilung in mehrere Testebenen:
| Ebene | Datenquelle | Hauptprüfung | Ausführungshäufigkeit |
|---|---|---|---|
| Unit-Regression | Lokale JSON-Fixtures | Decodierung und Fehlerzuordnung | Bei jedem Commit |
| Regression der Netzwerkschicht | URLProtocol | Vollständiger Anfrage-Antwort-Zyklus | Bei jedem Commit |
| Vertragstest | Kontrollierte Test-API | Feld- und Authentifizierungsvertrag | Zeitgesteuert oder vor Releases |
Offline-Regressionstests sollen irrelevante Schwankungen beseitigen, nicht eine Anfrage vortäuschen, die immer erfolgreich ist. 404, 429, 500, leere Antworten und beschädigtes JSON sollten ebenfalls als feste Testfälle vorliegen.
Eine separate URLSession injizieren
Das Protokoll sollte in Tests nicht global registriert werden. Ebenso sollte der Anwendungscode nicht direkt auf URLSession.shared zugreifen. Stattdessen erhält der APIClient eine injizierte Session: In der Produktion wird die Standardkonfiguration verwendet, in Tests eine ephemeral-Konfiguration. So werden weder Festplatten-Cache und Cookies noch der Zustand alter Verbindungen übernommen.
final class StubURLProtocol: URLProtocol {
static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool {
request.url?.host == "api.test.invalid"
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
request
}
override func startLoading() {
guard let handler = Self.handler else {
client?.urlProtocol(self, didFailWithError: URLError(.resourceUnavailable))
return
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}
func makeTestSession() -> URLSession {
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [StubURLProtocol.self]
configuration.timeoutIntervalForRequest = 3
return URLSession(configuration: configuration)
}
canInit muss auf einen bestimmten Host oder einen benutzerdefinierten Request-Header beschränkt werden. Gibt die Methode bedingungslos true zurück, können auch andere URLSession-Instanzen im Testprozess abgefangen werden. Dadurch lassen sich Fehler nur noch schwer eingrenzen.
Response-Fixtures wie Quellcode pflegen
Fixtures sollten im Test-Target unter Fixtures/API/v1/ liegen und nach Ressource und Szenario benannt werden, beispielsweise projects-success.json, projects-empty.json und projects-malformed.json. Vollständige Netzwerkaufzeichnungen mit dynamischen Tokens, E-Mail-Adressen oder internen Adressen sollten nicht unverändert gespeichert werden.
Vor dem Commit lassen sich die Schlüssel einheitlich sortieren und die Syntax prüfen:
find Tests/Fixtures -name '*.json' -print0 |
while IFS= read -r -d '' file; do
tmp="${file}.tmp"
jq -S . "$file" > "$tmp" && mv "$tmp" "$file"
done
Auch erfolgreiche Testfälle müssen die Anfrage selbst überprüfen. Wird nur das resultierende Modell geprüft, kann unbemerkt bleiben, dass der Client POST statt GET verwendet, einen Versions-Header auslässt oder Query-Parameter doppelt codiert.
StubURLProtocol.handler = { request in
XCTAssertEqual(request.httpMethod, "GET")
XCTAssertEqual(request.url?.path, "/v1/projects")
XCTAssertEqual(request.value(forHTTPHeaderField: "Accept"), "application/json")
let data = try Data(contentsOf: fixtureURL("projects-success"))
let response = HTTPURLResponse(
url: try XCTUnwrap(request.url),
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": "application/json"]
)!
return (response, data)
}
Fehlerpfade nach Kategorien prüfen
Mindestens Transportfehler, HTTP-Fehler und Inhaltsfehler sollten abgedeckt sein. Für einen Transportfehler kann URLError(.timedOut) ausgelöst werden. Ein HTTP-Fehler sollte einen echten Statuscode und einen strukturierten Fehler-Body zurückgeben. Bei einem Inhaltsfehler lautet der Statuscode dagegen 200, während die Antwort fehlende Felder, falsche Datentypen oder abgeschnittenes JSON enthält. Auf Ebene der Geschäftslogik dürfen diese drei Kategorien nicht alle als derselbe „unbekannte Fehler“ erscheinen.
Timeout-Tests sollten nicht tatsächlich mehrere zehn Sekunden warten. Der Handler kann den entsprechenden Fehler direkt auslösen, sodass geprüft wird, ob das View Model in einen wiederholbaren Zustand wechselt. Bei 429 sollte zusätzlich kontrolliert werden, ob der Client Retry-After ausliest. Der Test darf jedoch nicht wirklich pausieren: Die Berechnung des Backoffs sollte als reine Funktion ausgelagert werden, deren Eingaben und Ergebnisse separat geprüft werden.
Nach jedem Test muss der Handler auf nil gesetzt werden. Werden Tests parallel ausgeführt, kann ein einzelner statischer Handler von einem anderen Testfall überschrieben werden. Der zuverlässigste Einstieg besteht darin, diese Suite seriell auszuführen. Ist Parallelität erforderlich, sollte eine Zuordnung von Request-IDs zu Response-Closures verwendet und die Registry durch einen Lock oder Actor geschützt werden.
Stabile Ausführung auf einem Cloud-Mac
Auf einem Cloud-Mac von DplyMini wird zunächst die Xcode-Auswahl festgelegt. Anschließend werden stets derselbe Workspace, dasselbe Scheme und derselbe Testplan verwendet. Ein dedizierter physischer Mac ist keine virtuelle Maschine, dennoch sollte DerivedData für die Tests explizit isoliert werden, damit verschiedene Branches keine Zwischenprodukte gemeinsam nutzen.
set -euo pipefail
sudo xcode-select -s /Applications/Xcode.app
rm -rf "$PWD/.derived-data"
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-testPlan NetworkRegression \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath "$PWD/.derived-data" \
-resultBundlePath "$PWD/TestResults/NetworkRegression.xcresult"
Vor der ersten Ausführung sollte mit xcodebuild -showdestinations geprüft werden, welche Simulatornamen in der aktuellen Laufzeitumgebung verfügbar sind. Gerätenamen vom persönlichen Rechner sollten nicht unverändert in das Skript übernommen werden. Bei Fehlern sind xcresult, Testprotokolle und die zugehörige Fixture-Version aufzubewahren; nur die letzten Zeilen der Konsolenausgabe zu sichern, reicht nicht aus.
Für die abschließende Abnahme müssen vier Bedingungen erfüllt sein: Die Offline-Suite läuft auch ohne externe Netzwerkverbindung vollständig durch; wiederholte Ausführungen liefern identische Ergebnisse; Änderungen an Fixtures sind im Code-Review klar erkennbar; und ein fehlgeschlagener echter Vertragstest hindert Entwickler nicht daran, eine Regression in der Clientlogik unabhängig zu beurteilen. Erst dann werden Netzwerktests von einer von der „API-Tagesform“ abhängigen Prüfung zu einem wartbaren Bestandteil des Engineering-Prozesses.
Häufig gestellte Fragen
Können URLProtocol-Tests reale API-Integrationstests vollständig ersetzen?
Nein. Request-Aufbau, Decoding und Fehlerabbildung lassen sich damit zuverlässig prüfen, doch einige reale Vertragstests bleiben für Gateway, Authentifizierung und Änderungen am Serverschema notwendig.
Warum sollte eine eigene URLSession in den Testgegenstand injiziert werden?
Eine injizierte ephemeral-Konfiguration begrenzt das Abfangen auf den Testgegenstand und verhindert Einflüsse durch gemeinsame Caches, Cookies oder global registrierte Protokolle.
Wie verhindert man Konflikte eines gemeinsamen Handlers bei parallelen Tests?
Die einfache Variante wird seriell ausgeführt. Für Parallelität werden Antworten nach Request-Kennung gespeichert und die Registry mit einem Lock oder Actor geschützt.
Cloud-Mac-Mietdauer passend zur Aufgabendauer wählen
Zwei M4-Konfigurationen können tageweise, wöchentlich, monatlich oder vierteljährlich gemietet werden. Maßgeblich sind die im Dashboard in Echtzeit angezeigten Knoten und Verfügbarkeitsinformationen.