Zeigt dieselbe Infokarte nach einem Kontowechsel weiterhin alte Inhalte an, muss das nicht am Netzwerkcache liegen: Möglicherweise wird das Objekt im Arbeitsspeicher nur unter der Ressourcenkennung abgelegt, ohne die Kontoidentität zu berücksichtigen. Für solche Regressionstests auf einem Cloud-Mac von ArmShell müssen Sie keinen Speicherdruck erzeugen. Testen Sie zunächst die Schlüsselidentität, den beim Schreiben gespeicherten Snapshot, die Methode zum Leeren und das Verhalten bei einem Cache-Miss. Beobachten Sie die systemseitige Verdrängung anschließend getrennt davon.
Zuerst den Cache-Vertrag festlegen
Dieser Artikel testet ausschließlich einen prozessinternen NSCache, keine Festplatten- oder Netzwerkantwort-Caches. Bereiten Sie ein iOS-Unit-Test-Target mit XCTest vor und legen Sie die folgenden Typen und Tests in derselben Testdatei ab. Erstellen Sie den Cache bei jedem Start des Testprozesses neu, statt einen globalen Singleton wiederzuverwenden.
Der Schlüssel soll aus Konto, Ressourcenkennung und Inhaltsversion bestehen. Dieselbe Kennung darf unter verschiedenen Konten nicht zur selben Cache-Entry führen; nach einem Versionswechsel dürfen alte Daten nicht als Ergebnis für die neue Version dienen. Die Werte werden als Data gespeichert. Verändert der Aufrufer seine eigene Variable nach dem Schreiben weiter, darf dies den Snapshot im Cache nicht verändern.
Ein Cache ist eine Beschleunigungsschicht, deren Inhalte jederzeit verloren gehen können, keine maßgebliche Datenquelle. Bei jedem Lesezugriff muss ein Cache-Miss zulässig sein. Die Anwendung darf für ihre korrekte Funktion nicht darauf angewiesen sein, dass ein Objekt dauerhaft im Cache bleibt.
Passwörter, Tokens oder vollständige Benutzerprofile werden hier nicht getestet. Die Testdaten bestehen nur aus kurzen Texten; Fehlerprotokolle halten lediglich den Namen des Testfalls und den Trefferstatus fest.
Schlüssel aus der vollständigen Identität bilden
Verketten Sie die drei Felder nicht einfach mit Bindestrichen: Enthält ein Feld selbst das Trennzeichen, kann das Ergebnis mehrdeutig werden. Das folgende Beispiel bildet die Schlüsselidentität über Objektgleichheit ab. Hash-Kollisionen sind möglich, die Gleichheitsprüfung muss jedoch jedes Feld einzeln vergleichen. NSCache kopiert Schlüssel nicht automatisch; deshalb bleiben die Schlüsselfelder unveränderlich.
import Foundation
import XCTest
final class MemoryKey: NSObject {
let account: String
let resource: String
let revision: String
init(_ account: String, _ resource: String, _ revision: String) {
self.account = account
self.resource = resource
self.revision = revision
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(account)
hasher.combine(resource)
hasher.combine(revision)
return hasher.finalize()
}
override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? MemoryKey else { return false }
return account == other.account
&& resource == other.resource
&& revision == other.revision
}
}
@MainActor
final class SessionMemoryCache {
private let storage = NSCache<MemoryKey, NSData>()
init() {
storage.totalCostLimit = 4 * 1024 * 1024
}
func store(_ data: Data, for key: MemoryKey) {
storage.setObject(
NSData(data: data),
forKey: key,
cost: data.count
)
}
func read(_ key: MemoryKey) -> Data? {
guard let value = storage.object(forKey: key) else { return nil }
return Data(referencing: value)
}
func clear() {
storage.removeAllObjects()
}
}
Im Beispiel ist die Wrapper-Schicht auf den Main Actor beschränkt, damit sie sich konsistent mit dem Zustand der Benutzeroberfläche verwenden lässt. Das bedeutet nicht, dass NSCache selbst nur vom Hauptthread aus zugänglich wäre. Wenn Hintergrundzugriffe erforderlich sind, definieren Sie dafür ein eigenes Isolationskonzept, statt die Einschränkung kurzerhand zu entfernen.
Isolation, Versionen und Snapshots testen
Die erste Testgruppe verwendet absichtlich dieselbe Ressourcenkennung für unterschiedliche Konten und liest mit neu erzeugten, gleichwertigen Schlüsseln. So belegt der Test nicht bloß, dass „dasselbe Schlüsselobjekt den Wert wieder auslesen kann“. Die zweite Gruppe verändert das ursprüngliche Data und prüft anschließend den Cache-Wert sowie das explizite Leeren.
final class SessionMemoryCacheTests: XCTestCase {
@MainActor
func testIdentityIsolation() {
let cache = SessionMemoryCache()
cache.store(Data([1]), for: MemoryKey("a", "card", "v1"))
cache.store(Data([2]), for: MemoryKey("b", "card", "v1"))
XCTAssertEqual(
cache.read(MemoryKey("a", "card", "v1")), Data([1])
)
XCTAssertEqual(
cache.read(MemoryKey("b", "card", "v1")), Data([2])
)
XCTAssertNil(cache.read(MemoryKey("a", "card", "v2")))
}
@MainActor
func testSnapshotAndClear() {
let cache = SessionMemoryCache()
let key = MemoryKey("a", "card", "v1")
var source = Data([1, 2])
cache.store(source, for: key)
source.append(3)
XCTAssertEqual(cache.read(key), Data([1, 2]))
cache.clear()
XCTAssertNil(cache.read(key))
}
}
Diese kleinen Objekte dienen funktionalen Regressionstests bei normaler Ressourcenverfügbarkeit. Verdrängt das System zufällig dennoch einen Eintrag, können die Treffer-Assertions fehlschlagen. Für streng deterministische Tests der Geschäftslogik sollten Sie einen auf einem Dictionary basierenden Cache-Test-Dummy injizieren. Machen Sie nicht die gesamte Testsuite davon abhängig, dass das System die Objekte im Cache behält.
Ergänzen Sie außerdem einen Test für das Überschreiben unter demselben Schlüssel: Nach zwei aufeinanderfolgenden Schreibzugriffen muss der Lesezugriff die Daten des zweiten Schreibzugriffs liefern. Unterscheiden Sie auch zwischen einem leeren Data und einem Cache-Miss: Ersteres ist ein gültiger Wert, Letzterer wird durch nil dargestellt.
Kapazitätshinweise nicht als Verdrängungsalgorithmus verstehen
Im Beispiel werden die Kosten in Bytes angegeben, weil bei jedem Schreibzugriff data.count übergeben wird. Die Einheit von totalCostLimit legt der Aufrufer fest; sie entspricht nicht automatisch dem gemessenen Speicherverbrauch des Prozesses. Objekte, Schlüssel und der Container selbst verursachen zusätzlichen Speicherbedarf. Wird ein Bild in dekodierter Form gespeichert, lässt sich sein tatsächlicher Speicherbedarf ebenfalls nicht durch die Größe der komprimierten Datei ausdrücken.
Weder countLimit noch totalCostLimit sind harte Obergrenzen. Nach dem Einfügen mehrerer Objekte dürfen Sie nicht voraussetzen, dass das zuerst geschriebene Objekt mit Sicherheit verschwunden ist. Ebenso wenig dürfen Sie annehmen, dass NSCache eine deterministische Least-Recently-Used-Strategie implementiert. Versuche unter Speicherdruck sollten nur die Trefferquote und die Veränderung des Prozessspeichers dokumentieren. Sie eignen sich nicht als Tests mit festem Soll-Ergebnis, die vor jedem Merge bestehen müssen.
Versuchen Sie auch nicht, durch unbegrenztes Allozieren großer Arrays eine Bereinigung durch das System auszulösen: Dabei wird leicht zuerst der Testprozess beendet. Das belegt lediglich eine Erschöpfung der Ressourcen, nicht die Einhaltung des Cache-Vertrags.
Cache-Misses und Abmeldung in die Abnahmetests einbeziehen
Injizieren Sie in die lesende Schicht der Geschäftslogik einen Cache-Test-Dummy, der immer nil zurückgibt, und prüfen Sie, ob die Ladekomponente die Daten trotzdem aus der maßgeblichen Quelle bezieht. Injizieren Sie danach einen Test-Dummy mit einem bereits vorhandenen Wert und prüfen Sie, ob der Zweig, der den Cache verwenden darf, auf erneutes Laden verzichtet. Damit lassen sich beide Pfade direkt prüfen, ohne auf eine tatsächliche Verdrängung zu warten.
Stoppen Sie bei der Abmeldung zuerst die Anfragen des bisherigen Kontos und aktualisieren Sie die Sitzungsidentität. Leeren Sie erst danach den Cache. Das Leeren allein reicht nicht aus: Wird eine alte Anfrage anschließend abgeschlossen, könnte sie wieder Daten in den Cache schreiben. Vergleichen Sie vor dem Schreiben die von der Anfrage erfasste Sitzungsgeneration mit der aktuellen Generation und verwerfen Sie den Schreibvorgang, wenn sie nicht übereinstimmen. Diese Regel gehört in die Koordinationsschicht für das Laden und sollte unabhängig getestet werden.
Prüfen Sie abschließend vier Punkte: Stammt das Konto im Schlüssel aus einer vertrauenswürdigen Sitzung? Wird das Versionsfeld bei Inhaltsänderungen aktualisiert? Können alte Anfragen nach der Abmeldung noch Daten zurückschreiben? Gelingt das Laden auch bei einem Cache-Miss? Nehmen Sie stabile Assertions zur Geschäftslogik in die regelmäßigen Regressionstests auf und nutzen Sie instabile Beobachtungen unter Speicherdruck nur zur Diagnose. So wird der Cache nicht zu einer versteckten Voraussetzung, die sporadische Fehler verursacht.
Häufig gestellte Fragen
Garantiert countLimit eine genaue Objektanzahl?
Nein. countLimit und totalCostLimit sind keine harten Grenzen für exakte Assertions. Prüfen Sie den Umgang mit Cache-Misses. Eine feste Kapazität und Reihenfolge erfordern eine eigene deterministische Strategie.
Wie lässt sich eine Vermischung von Kontodaten verhindern?
Jeder Schlüssel braucht einen Kontonamensraum. Leeren Sie den Cache beim Abmelden und testen Sie zwei Konten mit derselben Ressourcenkennung auf getrennte Ergebnisse.
Bereit für Ihren nächsten Build mit einem Cloud-Mac
Wählen Sie Modell und Laufzeit passend zu Ihrer Arbeitslast. Dedizierte physische Apple-Silicon-Knoten statt virtueller Maschinen.
Cloud-Mac mieten