Si une même fiche affiche encore l’ancien contenu après un changement de compte, le cache réseau n’est pas forcément en cause : l’objet en mémoire est peut-être associé au seul identifiant de la ressource, sans tenir compte du compte. Pour exécuter ces tests de régression sur un Mac cloud ArmShell, inutile de provoquer une pression mémoire. Commencez par tester l’identité des clés, la conservation d’un instantané à l’écriture, la méthode de purge et le comportement en cas d’absence dans le cache. Observez ensuite, séparément, les évictions effectuées par le système.
Définir d’abord le contrat du cache
Cet article teste uniquement un NSCache en mémoire dans le processus, sans couvrir les caches disque ni les caches de réponses réseau. Préparez une cible de tests unitaires iOS avec XCTest et placez les types et les tests ci-dessous dans un même fichier de test. Recréez le cache à chaque lancement du processus de test, sans réutiliser de singleton global.
La clé doit réunir le compte, l’identifiant de la ressource et la version du contenu. Un même identifiant utilisé par deux comptes différents ne doit pas donner accès à la même entrée ; après un changement de version, les anciennes données ne doivent pas être renvoyées comme résultat de la nouvelle version. Les valeurs sont stockées sous forme de Data. Si l’appelant continue à modifier sa propre variable après l’écriture, l’instantané conservé dans le cache doit rester inchangé.
Le cache est une couche d’accélération dont les données peuvent disparaître, pas une source de vérité. Toute lecture doit pouvoir ne rien trouver, et le bon fonctionnement de l’application ne doit jamais dépendre de la présence permanente d’un objet dans le cache.
Nous ne testons ici ni mots de passe, ni jetons, ni profils utilisateur complets. Les jeux de données de test ne contiennent que de courts textes, et les journaux d’échec consignent uniquement le nom du cas de test et la présence ou l’absence de l’entrée.
Construire une clé à partir de l’identité complète
Ne concaténez pas simplement les trois champs avec des tirets : si un champ contient lui-même le séparateur, le résultat peut être ambigu. L’exemple suivant exprime l’identité de la clé au moyen de l’égalité entre objets. Les collisions de hachage sont possibles, mais l’égalité doit comparer chaque champ. NSCache ne copie pas les clés à votre place ; leurs champs restent donc immuables.
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()
}
}
Dans cet exemple, la couche d’encapsulation est isolée sur l’acteur principal afin de faciliter une utilisation cohérente avec l’état de l’interface. Cela ne signifie pas que NSCache lui-même ne peut être utilisé que depuis le thread principal. Si des accès en arrière-plan sont nécessaires, définissez un autre mode d’isolation plutôt que de supprimer cette contrainte sans précaution.
Tester l’isolation, les versions et les instantanés
Le premier groupe de tests utilise volontairement le même identifiant de ressource pour différents comptes et effectue les lectures avec de nouvelles clés équivalentes. On évite ainsi de démontrer seulement que « le même objet clé permet de relire la valeur ». Le second groupe modifie le Data d’origine, puis vérifie la valeur en cache et la purge explicite.
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))
}
}
Ces petits objets servent aux tests de régression fonctionnelle dans des conditions normales de disponibilité des ressources. Si le système procède malgré tout à une éviction, les assertions de présence peuvent échouer. Pour les tests strictement déterministes de la couche métier, injectez un double de cache implémenté avec un dictionnaire ; ne faites pas dépendre toute la suite de tests de la conservation des objets par le système.
Ajoutez également un cas de remplacement sous une même clé : après deux écritures successives, la lecture doit renvoyer les données de la seconde écriture. Distinguez aussi un Data vide d’une absence dans le cache : le premier est une valeur valide, la seconde est représentée par nil.
Ne pas confondre indications de capacité et algorithme d’éviction
Dans cet exemple, le coût est exprimé en octets, car chaque écriture transmet data.count. L’unité de totalCostLimit est définie par l’appelant ; cette propriété ne mesure pas automatiquement la mémoire du processus. Les objets, les clés et le conteneur lui-même entraînent des coûts supplémentaires. De même, si une image est stockée sous sa forme décodée, la taille du fichier compressé ne représente pas son coût réel.
Ni countLimit ni totalCostLimit ne constituent des limites strictes. Après avoir inséré plusieurs objets, vous ne pouvez pas affirmer que le premier inséré aura nécessairement disparu, ni supposer que NSCache applique une politique déterministe d’éviction de l’élément le moins récemment utilisé. Les essais sous pression doivent seulement rendre compte du taux de succès des lectures et de l’évolution de la mémoire du processus, sans imposer un résultat fixe à valider avant chaque fusion.
Évitez également d’allouer indéfiniment de grands tableaux pour déclencher le nettoyage par le système : vous risquez surtout de faire arrêter le processus de test. Vous aurez alors constaté un épuisement des ressources, pas établi le respect du contrat du cache.
Intégrer les absences du cache et la déconnexion aux tests de validation
Injectez dans la couche de lecture métier un double de cache qui renvoie toujours nil, puis vérifiez que le composant de chargement obtient tout de même les données auprès de la source de référence. Injectez ensuite un double contenant déjà une valeur et vérifiez que la branche autorisée à utiliser le cache ne relance pas le chargement. Vous contrôlez ainsi directement les deux chemins, sans attendre une éviction réelle.
Lors de la déconnexion d’un compte, arrêtez d’abord les requêtes de l’ancien compte et mettez à jour l’identité de session, puis videz le cache. Le vider ne suffit pas : une ancienne requête qui se termine ensuite pourrait encore y réécrire des données. Avant toute écriture, comparez le numéro de génération de session capturé par la requête à celui de la session courante et renoncez à l’écriture s’ils diffèrent. Cette règle relève de la couche de coordination du chargement et doit être testée séparément.
Vérifiez enfin quatre points : le compte utilisé dans la clé provient-il d’une session fiable, le champ de version évolue-t-il avec le contenu, une ancienne requête peut-elle réécrire dans le cache après la déconnexion, et le chargement aboutit-il même en l’absence de données en cache ? Intégrez les assertions métier stables aux tests de régression courants et réservez les observations instables sous pression au diagnostic. Le cache ne deviendra ainsi pas une condition implicite à l’origine de pannes intermittentes.
Questions fréquentes
countLimit impose-t-il un nombre exact d’objets ?
Non. countLimit et totalCostLimit ne sont pas des plafonds stricts permettant des assertions exactes. Testez le comportement en cas d’absence ; une politique déterministe exige une implémentation dédiée.
Comment empêcher la lecture du cache d’un autre compte ?
Incluez l’espace de noms du compte dans chaque clé et videz explicitement le cache à la déconnexion. Testez deux comptes utilisant le même identifiant de ressource.
Préparez votre prochain build sur un Mac dans le cloud
Choisissez le modèle et la durée selon votre charge de travail. Profitez d’un nœud physique Apple Silicon dédié, et non d’une machine virtuelle.
Louer un Mac dans le cloud