Tester l’identité des collections iOS : Hashable et mises à jour

Tester l’identité des collections iOS : Hashable et mises à jour

Tester l’identité des collections iOS : Hashable et mises à jour

Vous récupérez à nouveau le même enregistrement depuis le serveur : son nom a changé, mais une ligne supplémentaire apparaît dans la liste. Ailleurs, vous l’utilisez comme clé dans un Dictionary et ne retrouvez plus son état précédent. Avant de réécrire la logique de rafraîchissement, testez séparément l’égalité du modèle et son comportement dans les collections sur un Mac cloud. Cela suffit souvent à repérer une erreur dans la définition de son identité. Ici, nous testons uniquement un modèle de type valeur, sans simulateur ni connexion à une API réelle.

Définir ce qui constitue un même enregistrement

Supposons que l’application gère les membres de plusieurs locataires : l’identifiant d’un membre n’est unique qu’au sein de son locataire, et son nom peut changer. Son identité métier est donc composée de « l’identifiant du locataire + l’identifiant du membre » ; le nom n’en fait pas partie. Consignez d’abord cette règle dans la description des tests, puis implémentez les protocoles.

Il faut distinguer deux questions : deux jeux de données représentent-ils le même membre, et tous leurs champs sont-ils identiques ? Si vous laissez Swift synthétiser automatiquement la conformité d’une structure à Hashable, tous les champs stockés participent généralement à la comparaison. Après un changement de nom, les deux instantanés ne sont donc plus égaux.

Ne modifiez pas arbitrairement la sémantique d’égalité de tous vos modèles pour éliminer les doublons. Si vous devez comparer des instantanés complets, une conception plus robuste consiste à définir une clé d’identité immuable distincte, puis à utiliser un dictionnaire associant cette clé à l’enregistrement complet. Dans cet article, le modèle est égal selon son identité afin de rendre le contrat directement visible.

Des valeurs égales doivent suivre des règles de hachage cohérentes ; des valeurs ayant le même hachage ne sont pas nécessairement égales. Le hachage sert à réduire le champ de recherche, pas à fournir un identifiant permanent d’enregistrement.

Fixer les règles d’égalité et de hachage dans un script minimal

Vérifiez que les outils en ligne de commande de Xcode sont disponibles sur le Mac cloud. Exécutez xcrun swift --version et consignez la sortie avec l’identifiant du commit. Créez identity-contract.swift avec le contenu ci-dessous. Cet exemple n’a aucune dépendance externe et ne lit aucun identifiant d’authentification.

struct Member: Hashable {
    let tenantID: String
    let memberID: String
    var displayName: String

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.tenantID == rhs.tenantID &&
        lhs.memberID == rhs.memberID
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(tenantID)
        hasher.combine(memberID)
    }
}

let original = Member(
    tenantID: "team-a",
    memberID: "member-7",
    displayName: "Old"
)
var renamed = original
renamed.displayName = "New"

let otherTenant = Member(
    tenantID: "team-b",
    memberID: "member-7",
    displayName: "Old"
)

precondition(original == renamed, "Rename changed identity")
precondition(original.hashValue == renamed.hashValue,
             "Equal values hash differently")
precondition(original != otherTenant, "Tenant boundary lost")

var members: Set<Member> = [original]
precondition(!members.insert(renamed).inserted,
             "Duplicate identity inserted")
precondition(members.count == 1, "Unexpected member count")
precondition(members.first?.displayName == "Old",
             "Insert unexpectedly replaced the member")

let previous = members.update(with: renamed)
precondition(previous?.displayName == "Old",
             "Previous member missing")
precondition(members.count == 1, "Update changed member count")
precondition(members.first?.displayName == "New",
             "Updated fields missing")

members.insert(otherTenant)
precondition(members.count == 2, "Distinct tenants merged")

let states: [Member: String] = [original: "selected"]
precondition(states[renamed] == "selected",
             "Lookup failed after rename")
precondition(states[otherTenant] == nil,
             "Lookup crossed tenant boundary")

La comparaison d’égalité et hash(into:) utilisent les mêmes champs d’identité. Ceux-ci sont déclarés avec let, tandis que les champs d’affichage restent modifiables. Vérifiez tout particulièrement une erreur discrète : ignorer le nom dans la comparaison, mais l’inclure dans le hachage. Cela viole le contrat du protocole, sans pour autant se manifester immédiatement à chaque exécution.

Vérifier la déduplication et le contenu après mise à jour

Dans le répertoire du script, exécutez :

set -eu
xcrun swiftc -Onone identity-contract.swift -o identity-contract
./identity-contract

Si tout est correct, le script se termine sans rien afficher ; si une précondition échoue, il renvoie un code de sortie non nul. Conservez -Onone et n’utilisez pas -Ounchecked, qui désactive les vérifications de sécurité. Pour intégrer ce test à un pipeline existant, faites dépendre sa réussite directement du code de sortie de cette commande.

Vérifier uniquement que le Set contient un élément ne suffit pas. Lorsque insert rencontre un membre égal à celui qui est déjà présent, il ne remplace pas son contenu. Une déduplication réussie ne garantit donc pas que le nom a été mis à jour. L’exemple vérifie d’abord que l’ancien nom est toujours présent, puis utilise update(with:) pour s’assurer que le nouveau nom a bien été enregistré.

Le test du Dictionary répond à une autre question : la clé dont le nom a changé permet-elle toujours de retrouver l’état initial ? Si vous devez conserver l’enregistrement complet le plus récent, il est préférable de le stocker dans la valeur du dictionnaire et de réserver la clé à l’identité, sans y inclure les données d’affichage.

Couvrir les cas limites sans tester des valeurs de hachage précises

Séparer les règles d’identité des règles d’entrée

C’est le contrat de l’API qui doit déterminer si les identifiants textuels sont sensibles à la casse et si les valeurs vides sont autorisées. N’effectuez pas de conversion implicite dans la fonction de hachage. Si une normalisation est nécessaire, réalisez-la avant la création du modèle : la comparaison d’égalité et le hachage doivent utiliser le même résultat normalisé.

Ajoutez les vérifications suivantes aux tests du projet :

N’imposez pas qu’un hashValue soit égal à un entier fixe, ni que des identités distinctes aient toujours des hachages différents. La randomisation entre processus et les collisions légitimes rendent ces tests sans intérêt. Relancer le script peut aider à détecter un comportement instable, mais ne remplace pas la revue des règles applicables aux champs.

Appliquer le contrat au modèle de production et aux contrôles avant fusion

Le script autonome ne valide que la logique de l’exemple. Lors de l’intégration au projet, transférez les assertions dans la cible de tests et importez directement le modèle de production. Ne conservez pas durablement une copie réservée aux tests : une erreur introduite dans le code métier pourrait alors laisser le script passer.

Si le modèle actuel est un type référence, il est particulièrement important de ne pas modifier les champs participant au hachage tant que l’objet appartient à une collection. Celle-ci ne reconstruit pas automatiquement son index lorsque l’état interne de l’objet change. Privilégiez une clé d’identité immuable ; si l’identité doit réellement changer, retirez d’abord l’ancien membre, puis créez et insérez le nouveau.

Enfin, examinez la persistance et l’état de sélection dans l’interface : ils doivent stocker des identifiants métier explicites, et non des valeurs de hachage ou des positions dans l’ordre d’itération d’une collection. Les critères de validation sont concrets : un changement de nom ne crée pas de doublon, le nouveau contenu est accessible après mise à jour, les membres de locataires différents ne sont pas fusionnés, et la recherche comme la suppression restent correctes.

Questions fréquentes

Deux enregistrements différents peuvent-ils avoir le même hashValue ?

Oui. Les collisions sont autorisées : les collections distinguent ensuite les valeurs par leur égalité. Des valeurs égales doivent être hachées de façon cohérente, mais des identités différentes peuvent partager un hachage.

Set remplace-t-il automatiquement un enregistrement renommé ?

Non. Modifier une copie extérieure au Set ne change pas son élément stocké. Utilisez update(with:), puis vérifiez que le nombre d’éléments reste identique et que les nouveaux champs sont présents.

Peut-on conserver hashValue comme clé de base de données ?

Non. Le hachage Swift est aléatoire et sa stabilité entre processus ou versions n’est pas garantie. Utilisez un identifiant serveur ou un UUID, complété par l’identifiant du locataire si nécessaire.

Un Mac dédié pour vos projets

Des nœuds Mac mini physiques, pas des machines virtuelles. Louez à la journée, à la semaine, au mois ou au trimestre. Choisissez votre usage et votre configuration, puis vérifiez la disponibilité en temps réel dans la console.

Choisir un modèle et une durée Voir tous les tarifs