Tester les préférences iOS sans état résiduel sur Mac cloud
La même série de tests de préférences iOS réussit au premier passage, puis relit au deuxième le thème laissé par l’exécution précédente. Une fois les tests déplacés sur un Mac cloud, le résultat change encore. Le problème vient généralement non pas des performances de la machine, mais d’une confusion entre les valeurs par défaut propres au processus, les préférences enregistrées et les arguments de lancement. Pour ce type de tâche sur VpsKeep, commencez par isoler les préférences, puis vérifiez le comportement applicatif. Effacer tout le simulateur en espérant résoudre le problème n’est pas une méthode fiable.
Définir d’abord le contrat de lecture
UserDefaults ne se résume pas à une seule table de paires clé-valeur. La lecture parcourt plusieurs domaines : les arguments de lancement peuvent prendre le pas sur le domaine de l’application, tandis que le domaine d’enregistrement fournit les valeurs de repli. Obtenir une valeur avec object(forKey:) ne signifie donc pas qu’elle a été enregistrée.
Pour chaque préférence, définissez d’abord trois éléments : le nom de la clé, le type accepté et le résultat attendu en l’absence de valeur. Par exemple, la clé du thème n’accepte que system, light et dark. Si la valeur est absente, d’un type incorrect ou constituée d’une chaîne inconnue, la lecture renvoie toujours system. Les valeurs invalides laissées par d’anciennes versions ont ainsi, elles aussi, un comportement déterministe.
register(defaults:) ne fait qu’enregistrer des valeurs de repli pour le processus en cours. Cette méthode ne les écrit pas comme préférences utilisateur. Retrouver la valeur par défaut après avoir supprimé une valeur persistante correspond à un repli normal, pas à un échec de la suppression.
N’utilisez pas
dictionaryRepresentation()pour prouver qu’une clé a été enregistrée. Cette méthode regroupe les préférences visibles et peut inclure des valeurs du domaine d’enregistrement. Pour vérifier ce qui est réellement enregistré, lisez le domaine persistant concerné.
Injecter une suite isolée sans toucher aux vraies préférences
Le code de production peut utiliser .standard par défaut, mais les tests doivent injecter une instance dédiée. La couche qui encapsule les préférences ne doit pas récupérer à nouveau .standard à l’intérieur de ses méthodes : l’injection de dépendances ne fournirait alors qu’une isolation de façade.
Le code ci-dessous peut être placé dans la cible de tests unitaires iOS. Chaque test utilise un nom de suite aléatoire et ne supprime que le domaine persistant qu’il a lui-même créé. Dans cet exemple, la couche qui encapsule la lecture figure également dans le fichier de test. Lors de l’intégration au projet, déplacez-la dans la cible de l’application, puis importez-la depuis les tests.
import Foundation
import XCTest
struct ThemePreferences {
let defaults: UserDefaults
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
defaults.register(defaults: ["theme": "system"])
}
var theme: String {
guard let value = defaults.object(forKey: "theme") as? String,
["system", "light", "dark"].contains(value) else {
return "system"
}
return value
}
}
final class ThemePreferencesTests: XCTestCase {
private func withSuite(
_ body: (UserDefaults, String) throws -> Void
) throws {
let name = "preferences-tests." + UUID().uuidString
let defaults = try XCTUnwrap(UserDefaults(suiteName: name))
defaults.removePersistentDomain(forName: name)
defer {
defaults.removePersistentDomain(forName: name)
}
try body(defaults, name)
}
func testOverrideAndRemoval() throws {
try withSuite { defaults, name in
let preferences = ThemePreferences(defaults: defaults)
XCTAssertEqual(preferences.theme, "system")
XCTAssertNil(
defaults.persistentDomain(forName: name)?["theme"]
)
defaults.set("dark", forKey: "theme")
XCTAssertEqual(preferences.theme, "dark")
XCTAssertEqual(
defaults.persistentDomain(forName: name)?["theme"]
as? String,
"dark"
)
defaults.removeObject(forKey: "theme")
XCTAssertNil(
defaults.persistentDomain(forName: name)?["theme"]
)
XCTAssertEqual(preferences.theme, "system")
}
}
func testInvalidValuesFallBack() throws {
try withSuite { defaults, _ in
let preferences = ThemePreferences(defaults: defaults)
defaults.set(["dark"], forKey: "theme")
XCTAssertEqual(preferences.theme, "system")
defaults.set("unknown", forKey: "theme")
XCTAssertEqual(preferences.theme, "system")
}
}
}
Les noms aléatoires empêchent les tests exécutés en parallèle d’écraser les données les uns des autres. Le nettoyage en sortie couvre le déroulement normal ; si le processus est arrêté de force, l’exécution de defer n’est pas garantie. Les domaines aléatoires résiduels ne seront pas réutilisés au passage suivant, mais les appareils de test utilisés durablement doivent tout de même faire l’objet d’un inventaire régulier. Ne supprimez que les domaines dont l’appartenance aux tests est confirmée.
Couvrir les cas limites du remplacement et de la suppression
Les vérifications minimales sont les suivantes : repli en l’absence de valeur enregistrée, remplacement par une valeur valide, repli après suppression, repli en cas de type incorrect et repli pour une valeur d’énumération inconnue. Ajoutez aussi un test vérifiant que la valeur enregistrée reste lisible après la recréation de la couche applicative qui encapsule les préférences. Cela évite qu’un appel à set à chaque initialisation ne réinitialise le choix de l’utilisateur.
Si l’application utilise à la fois une suite partagée et son propre domaine, n’appelez pas addSuite(named:) sans précaution dans les tests unitaires. Cette méthode modifie le chemin de recherche et rend les résultats dépendants d’autres domaines. Traitez d’abord les préférences partagées comme une autre dépendance injectable, validez les deux séparément, puis passez aux tests d’intégration de l’application.
Pour les préférences booléennes, il faut également distinguer « non défini » de « désactivé par l’utilisateur ». Un simple appel à bool(forKey:) renvoie false lorsque la valeur est absente : il ne permet pas de savoir si l’utilisateur a déjà fait un choix. Vérifiez explicitement la présence de la clé ou ajoutez un champ d’état distinct et versionné.
Vérifier la persistance réelle avec deux processus
Créer un nouvel objet UserDefaults dans le même processus ne valide que le comportement de lecture, pas la persistance après un redémarrage. Le test d’intégration doit comporter une phase d’écriture et une phase de lecture, toutes deux utilisant la même suite de test au nom fixe.
Lors de la première phase, écrivez le thème, confirmez sa relecture depuis un écran de l’application ou une interface de test, puis laissez l’application se terminer normalement. Lors de la seconde phase, lancez l’application sans réécrire la valeur et vérifiez directement que le thème est restauré. Terminez par une phase de nettoyage indépendante qui supprime ce domaine de test.
Un arrêt forcé suivi d’une lecture immédiate ne constitue pas une validation ordinaire de l’enregistrement : il teste le cas limite d’une terminaison anormale. Ne considérez pas non plus qu’une attente fixe de quelques secondes ou un appel à synchronize() prouve que l’écriture sur disque est terminée.
Les arguments de lancement relèvent d’une série de tests distincte. Si l’application injecte le thème par ces arguments, vérifiez séparément qu’ils prennent bien le pas sur les autres valeurs, puis retirez-les lors des lancements suivants. Sinon, ils masquent la valeur enregistrée et peuvent donner un faux résultat positif au test de persistance.
Consigner les échecs pour pouvoir les retracer
Dans le pipeline du Mac cloud, il suffit de consigner le nom du test, le nom de la suite, la version de l’application, la présence ou l’absence de la clé, le type réel et la valeur de repli attendue. N’exportez pas directement l’ensemble du dictionnaire des préférences : il peut contenir des identifiants utilisateur ou des informations sur l’environnement interne.
Exécutez les tests deux fois de suite, puis activez l’exécution parallèle et vérifiez que les résultats restent identiques. Si seul le deuxième passage échoue, cherchez d’abord les suites au nom fixe et les domaines non nettoyés. Si l’échec n’apparaît qu’en parallèle, examinez les singletons partagés. S’il ne survient qu’après un redémarrage, vérifiez que l’initialisation n’écrase pas les valeurs de l’utilisateur.
La validation finale ne consiste pas à avoir « lu la bonne chaîne une fois ». Les valeurs par défaut, leur remplacement par les choix de l’utilisateur, le repli après suppression et le redémarrage du processus doivent chacun disposer de preuves distinctes. Une fois ces limites clairement établies, les tests de préférences peuvent aboutir aux mêmes conclusions sur une nouvelle machine comme sur des nœuds utilisés durablement.
Questions fréquentes
register(defaults:) enregistre-t-il les valeurs sur disque ?
Non. Il remplit le domaine d’enregistrement du processus courant. L’application doit enregistrer ces valeurs à nouveau lors du prochain lancement.
Un nouvel objet UserDefaults suffit-il à prouver la persistance ?
Non. Des objets du même processus peuvent partager leur état. Terminez normalement le processus qui écrit, puis lisez les valeurs dans un nouveau processus.
Exécutez vos étapes sur un Mac cloud dédié
Louez un Mac mini physique complet, et non une machine virtuelle. Choisissez une durée à la journée, à la semaine, au mois ou au trimestre. Vérifiez le modèle et la région avant de confirmer votre commande ; la disponibilité réelle est indiquée en temps réel dans la console.