Wenn eine iOS-App komprimiertes JSON empfängt, zeigt der Netzwerkmitschnitt möglicherweise nur einige Dutzend KB an. Die Daten, die beim Parser ankommen, können jedoch deutlich größer sein. Um zu prüfen, was die App tatsächlich akzeptiert, musst du nicht auf einen Fehler der produktiven API warten: Starte auf einem Cloud-Mac einen Testserver, der nur lokal erreichbar ist, und rufe ihn mit dem iOS-Simulator auf demselben Mac auf. Anhand einer gültigen Antwort, einer Antwort mit beschädigter Prüfsumme und einer Antwort mit großer entpackter Datenmenge prüfst du, welche Daten URLSession zurückgibt und welche Grenze die App vor dem Parsen setzt.
Übertragungsgröße und App-Daten unterscheiden
Gzip komprimiert den Antwort-Body für die Übertragung. Content-Length kann die Anzahl der komprimiert übertragenen Bytes angeben; die Data, die der Anwendungscode erhält, können dagegen bereits den entpackten Body enthalten. Eine Grenze nur für die Übertragungsgröße verhindert nicht, dass wiederholte Zeichen oder sehr ähnliche JSON-Fragmente nach dem Entpacken viel Speicher belegen. In diesem Beispiel gilt für den entpackten API-Response ein Limit von 256 KiB. Das ist ein Testschwellenwert, kein allgemeingültiger Wert für alle iOS-APIs. Lege den passenden Grenzwert anhand der tatsächlichen Antwortgrößen fest.
Beim Testen musst du außerdem zwischen „Netzwerkaufruf erfolgreich“ und „Body verwendbar“ unterscheiden. Ein korrekter Statuscode und das Ausbleiben eines Timeouts ersetzen keine Prüfung des Inhalts. Insbesondere bei einem beschädigten komprimierten Datenstrom solltest du nicht voraussetzen, dass das System immer mit einem bestimmten Fehler reagiert. Entscheidend ist, dass die App beschädigte Daten nicht als vollständiges fachliches Ergebnis akzeptiert.
Die Testassertions sollten die von der App verwendeten Daten prüfen, nicht nur die Übertragungsheader oder die im Netzwerk-Panel angezeigte komprimierte Größe.
Drei lokale Gzip-Antworten erzeugen
Speichere das folgende Skript als /tmp/gzip_fixture.py und führe auf dem Cloud-Mac python3 /tmp/gzip_fixture.py aus. Der Server bindet sich ausschließlich an 127.0.0.1:8765 und ist nicht über das öffentliche Netz erreichbar. /ok liefert einen kurzen Body, /broken verändert die Prüfsumme am Ende des Gzip-Datenstroms, und /large sendet einen stark komprimierbaren Body, der entpackt 1 MiB groß ist. Alle drei Pfade senden tatsächlich komprimierte Daten und geben deren komprimierte Länge im Antwortheader an.
import gzip
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
SMALL = b'{"status":"ready"}'
LARGE = b"A" * 1_048_576
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path not in ("/ok", "/broken", "/large"):
self.send_error(404)
return
payload = LARGE if self.path == "/large" else SMALL
wire = bytearray(gzip.compress(payload))
if self.path == "/broken":
wire[-8] ^= 0xFF
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Encoding", "gzip")
self.send_header("Content-Length", str(len(wire)))
self.end_headers()
self.wfile.write(wire)
ThreadingHTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
Hier wird bewusst die Prüfsumme verändert, statt die TCP-Verbindung einfach abzubrechen. So lassen sich „vollständig übertragen, aber inhaltlich beschädigt“ und ein Verbindungsabbruch getrennt beobachten. Das Skript liefert feste Testdaten; es bildet weder die Kompressionsrate noch den Durchsatz einer realen API ab. Prüfe vor dem Test mit lsof -nP -iTCP:8765 -sTCP:LISTEN, ob tatsächlich der gerade gestartete Prozess auf dem Port lauscht. Ist der Port belegt, stoppe zuerst den alten Dienst, damit die Tests nicht versehentlich ein anderes Programm erreichen.
Den tatsächlichen Body im Simulator prüfen
Füge das folgende XCTest-Beispiel in ein bestehendes Test-Target des Projekts ein. Es gilt nur für einen Simulator auf demselben Mac, auf dem der Server läuft: 127.0.0.1 bezeichnet hier diesen Mac. Wenn du stattdessen ein physisches Gerät verwendest, musst du Serveradresse und Zugriffskontrolle neu planen. Das Testprojekt muss lokale HTTP-Anfragen in dieser Entwicklungsumgebung zulassen. Lockere dafür nicht die Sicherheitseinstellungen für entfernte Anfragen im Release-Build.
import Foundation
import XCTest
final class GzipResponseTests: XCTestCase {
private let base = URL(string: "http://127.0.0.1:8765")!
private func fetch(_ path: String) async throws -> Data {
let url = base.appendingPathComponent(path)
let (data, response) = try await URLSession.shared.data(from: url)
XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 200)
return data
}
func testValidBody() async throws {
let data = try await fetch("ok")
XCTAssertEqual(data, Data(#"{"status":"ready"}"#.utf8))
}
func testLargeDecodedBodyExceedsAppLimit() async throws {
let data = try await fetch("large")
XCTAssertGreaterThan(data.count, 256 * 1024)
}
func testDamagedBodyIsNotAcceptedAsComplete() async {
do {
let data = try await fetch("broken")
XCTAssertNotEqual(
data,
Data(#"{"status":"ready"}"#.utf8)
)
} catch {
return
}
}
}
Führe zuerst den Test mit dem gültigen Body aus, danach die Tests mit dem großen und dem beschädigten Body. Falls der erste Test statt des erwarteten Texts rohe Gzip-Bytes empfängt, prüfe zunächst die Antwortheader, die Testumgebung und die Implementierung der Anfrage. Ändere die Assertion nicht einfach zu „Hauptsache, es kommen Daten an“. Beim beschädigten Body sind zwei beobachtbare Ergebnisse zulässig: Der Aufruf schlägt fehl oder die zurückgegebenen Daten entsprechen nicht dem vollständigen erwarteten Body. Der Test garantiert nicht, dass das System eine falsche Prüfsumme in einer bestimmten Phase zurückweist, und ersetzt keine Prüfung der JSON-Felder auf Anwendungsebene.
Die Größenbegrenzung vor dem Parsen anwenden
Prüfe im Anwendungscode zuerst Data.count und übergib die Daten erst danach an den Decoder. Das folgende Beispiel setzt eine Grenze nach dem Empfang der vollständigen Antwort. Damit vermeidest du, einen zu großen Body weiterzuverarbeiten; die bereits beim Herunterladen und Entpacken entstandene Speicherbelegung verhindert es nicht. Falls die API sehr große Inhalte liefern kann, brauchst du in der App-Architektur zusätzlich einen Empfang in Teilen, eine Zwischenspeicherung auf dem Datenträger oder serverseitige Grenzen. Diese eine Prüfung reicht dafür nicht aus.
enum ResponseError: Error {
case tooLarge
}
func checkedBody(_ data: Data) throws -> Data {
guard data.count <= 256 * 1024 else {
throw ResponseError.tooLarge
}
return data
}
Erweitere den Test für den großen Body so, dass er prüft, ob checkedBody(data) den Fehler tooLarge auslöst. Der gültige Body muss die Prüfung bestehen. Damit testest du nicht mehr nur einen beobachtbaren Größenunterschied, sondern ob die App ihre Grenze tatsächlich durchsetzt. Falls deine API JSON liefert, dekodiere es erst nach der Größenprüfung und kontrolliere die erforderlichen Felder. Dass sich ein Body parsen lässt, beweist nicht, dass sein Inhalt von der erwarteten API stammt.
Reproduzierbare Prüfpunkte zum Abschluss festhalten
Prüfe vor dem Commit dieser Tests fünf Punkte: Der Server lauscht nur lokal; die Tests verwenden ausdrücklich einen Simulator auf demselben Mac; der gültige Body wird vollständig mit dem erwarteten Body verglichen; der beschädigte Body wird nicht als vollständiger Inhalt akzeptiert; und die Größenbegrenzung bezieht sich auf die Data, die URLSession der App übergibt. Stoppe anschließend den temporären Server, vergewissere dich, dass Port 8765 nicht mehr überwacht wird, und prüfe, dass die Ausnahme für lokales HTTP aus dem Test-Target nicht in den Release-Build gelangt ist.
Diese Test-Fixtures sind bewusst klein gehalten. Wenn du später leere Bodys, fehlerhafte Statuscodes oder andere Kodierungen abdecken möchtest, ergänze jeweils einen eigenen Pfad und eine eigene Assertion. Bündle nicht mehrere Fehlerfälle in einer Antwort, die sich gelegentlich ändert. Feste Eingaben, eine klare Grenze und die Prüfung der Bytes, die die App tatsächlich verwendet, sind verlässlicher als eine Beurteilung allein anhand der komprimierten Größe im Netzwerkmitschnitt.
Häufig gestellte Fragen
Reicht Content-Length als Größenlimit?
Nein. Der Header bezeichnet die übertragene, komprimierte Länge. Entscheidend ist die Größe der Data, die deine App nach dem Entpacken erhält.
Muss URLSession bei einer falschen Gzip-Prüfsumme immer werfen?
Der Test sollte keine bestimmte Fehlerform voraussetzen. Die App darf die beschädigte Antwort nicht als vollständigen Inhalt akzeptieren; ein Fehler oder eine fehlgeschlagene Inhaltsprüfung genügt.
Möchten Sie es selbst testen?
Mieten Sie einen dedizierten Mac mini und starten Sie Ihren Build direkt
Wählen Sie zwischen zwei M4-Servermodellen und legen Sie Mietdauer und Standort nach Ihrem Bedarf fest. Die aktuelle Verfügbarkeit sehen Sie direkt in der Konsole.