Comment une unité devrait-elle tester le contrat hashCode-equals?

En bref, le contrat hashCode, selon object.hashCode () de Java:

  1. Le code de hachage ne doit pas changer à moins que quelque chose affectant equals () ne change
  2. equals () implique que les codes de hachage sont ==

Assumons l’intérêt principalement dans les objects de données immuables – leurs informations ne changent jamais après leur construction, donc le # 1 est supposé tenir. Cela laisse # 2: le problème est simplement celui de confirmer qu’égale implique le code de hachage ==.

De toute évidence, nous ne pouvons pas tester tous les objects de données imaginables, sauf si cet ensemble est sortingvialement petit. Alors, quelle est la meilleure façon d’écrire un test unitaire susceptible d’attraper les cas les plus courants?

Les instances de cette classe étant immuables, il existe des moyens limités pour construire un tel object. ce test unitaire devrait tous les couvrir si possible. Au sumt de ma tête, les points d’entrée sont les constructeurs, la désérialisation et les constructeurs de sous-classes (qui devraient être réductibles au problème d’appel du constructeur).

[Je vais essayer de répondre à ma propre question par la recherche. La consortingbution d’autres StackOverflowers est un mécanisme de sécurité bienvenu pour ce processus.]

[Cela pourrait être applicable à d’autres langues OO, alors j’ajoute cette balise.]

EqualsVerifier est un projet open source relativement nouveau et il fait un très bon travail pour tester le contrat égal. Il n’a pas les problèmes d’ EqualsTester de GSBase. Je le recommanderai assurément.

Mon conseil serait de réfléchir à pourquoi / comment cela pourrait ne jamais être vrai, et ensuite écrire quelques tests unitaires qui ciblent ces situations.

Par exemple, supposons que vous ayez une classe Set personnalisée. Deux ensembles sont égaux s’ils contiennent les mêmes éléments, mais il est possible que les structures de données sous-jacentes de deux ensembles égaux diffèrent si ces éléments sont stockés dans un ordre différent. Par exemple:

 MySet s1 = new MySet( new Ssortingng[]{"Hello", "World"} ); MySet s2 = new MySet( new Ssortingng[]{"World", "Hello"} ); assertEquals(s1, s2); assertTrue( s1.hashCode()==s2.hashCode() ); 

Dans ce cas, l’ordre des éléments dans les ensembles peut affecter leur hachage, en fonction de l’algorithme de hachage que vous avez implémenté. Donc, c’est le genre de test que j’écrirai, car il teste le cas où je sais qu’il serait possible pour un algorithme de hachage de produire des résultats différents pour deux objects que j’ai définis égaux.

Vous devriez utiliser une norme similaire avec votre propre classe personnalisée, quelle qu’elle soit.

Cela vaut la peine d’utiliser les addons junit pour cela. Découvrez la classe EqualsHashCodeTestCase http://junit-addons.sourceforge.net/ que vous pouvez étendre et implémenter createInstance et createNotEqualInstance, cela vérifiera que les méthodes equals et hashCode sont correctes.

Je recommanderais le EqualsTester de GSBase. Il fait essentiellement ce que vous voulez. J’ai cependant deux problèmes mineurs:

  • Le constructeur fait tout le travail, ce que je ne considère pas comme une bonne pratique.
  • Il échoue lorsqu’une instance de classe A est égale à une instance d’une sous-classe de la classe A. Cela ne constitue pas nécessairement une violation du contrat égal.

[Au moment d’écrire ces lignes, trois autres réponses ont été postées.]

Pour rappel, le but de ma question est de trouver des cas standard de tests pour confirmer que hashCode et ses equals sont en accord. Mon approche de cette question est d’imaginer les chemins communs pris par les programmeurs lors de l’écriture des classes en question, à savoir les données immuables. Par exemple:

  1. A écrit equals() sans écrire hashCode() . Cela signifie souvent que l’égalité a été définie pour signifier l’égalité des champs de deux instances.
  2. Écrit hashCode() sans écrire equals() . Cela peut signifier que le programmeur cherchait un algorithme de hachage plus efficace.

Dans le cas de # 2, le problème me semble inexistant. Aucune autre instance n’a été faite equals() , de sorte qu’aucune instance supplémentaire ne doit avoir des codes de hachage égaux. Au pire, l’algorithme de hachage peut donner de moins bonnes performances pour les cartes de hachage, ce qui sort du cadre de cette question.

Dans le cas de # 1, le test d’unité standard consiste à créer deux instances du même object avec les mêmes données transmises au constructeur et à vérifier des codes de hachage égaux. Qu’en est-il des faux positifs? Il est possible de choisir des parameters de constructeur qui produisent des codes de hachage égaux sur un algorithme non fiable. Un test unitaire qui tend à éviter de tels parameters satisferait l’esprit de cette question. Le raccourci ici est d’inspecter le code source pour equals() , de réfléchir et de rédiger un test basé sur cela, mais bien que cela puisse être nécessaire dans certains cas, il peut également y avoir des tests courants qui détectent des problèmes courants. remplir l’esprit de cette question.

Par exemple, si la classe à tester (appelez-la Data) a un constructeur qui prend une chaîne et que les instances construites à partir de chaînes equals() généré des instances equals() , un bon test devrait probablement tester:

  • new Data("foo")
  • une autre new Data("foo")

Nous pourrions même vérifier le code de hachage pour les new Data(new Ssortingng("foo")) , pour forcer la chaîne à ne pas être internée, bien que cela ait plus de chances de donner un code de hachage correct que Data.equals() résultat, à mon avis.

La réponse d’Eli Courtwright est un exemple de reflection sur la manière de casser l’algorithme de hachage basé sur la connaissance de la spécification d’ equals . L’exemple d’une collection spéciale est un bon exemple, car les Collection créées par les utilisateurs apparaissent parfois, et sont très enclines à être mélangées dans l’algorithme de hachage.

C’est l’un des seuls cas où j’aurais plusieurs assertions dans un test. Comme vous devez tester la méthode égale, vous devez également vérifier la méthode hashCode en même temps. Donc, sur chacun des cas de test de votre méthode égale, vérifiez également le contrat hashCode.

 A one = new A(...); A two = new A(...); assertEquals("These should be equal", one, two); int oneCode = one.hashCode(); assertEquals("HashCodes should be equal", oneCode, two.hashCode()); assertEquals("HashCode should not change", oneCode, one.hashCode()); 

Et bien sûr, vérifier un bon hashCode est un autre exercice. Honnêtement, je ne prendrais pas la peine de faire la double vérification pour m’assurer que le hashCode ne change pas dans le même temps, ce genre de problème est mieux géré en le capturant dans une revue de code et en aidant le développeur à comprendre pourquoi écrire des méthodes hashCode.

Vous pouvez également utiliser quelque chose de similaire à http://code.google.com/p/guava-libraries/source/browse/guava-testlib/src/com/google/common/testing/EqualsTester.java pour tester égal et hashCode.

Si j’ai une classe Thing , comme la plupart des autres, j’écris une classe ThingTest , qui contient tous les tests unitaires pour cette classe. Chaque ThingTest a une méthode

  public static void checkInvariants(final Thing thing) { ... } 

et si la classe Thing remplace hashCode et égale à une méthode

  public static void checkInvariants(final Thing thing1, Thing thing2) { ObjectTest.checkInvariants(thing1, thing2); ... invariants that are specific to Thing } 

Cette méthode est chargée de vérifier tous les invariants conçus pour tenir entre deux paires d’objects Thing . La méthode ObjectTest qu’elle délègue est responsable de la vérification de tous les invariants qui doivent être ObjectTest entre deux paires d’objects. Comme equals et hashCode sont des méthodes de tous les objects, cette méthode vérifie que hashCode et ses equals sont cohérents.

J’ai ensuite quelques méthodes de test qui créent des paires d’objects Thing et les transmets à la méthode checkInvariants par paire. J’utilise le partitionnement d’équivalence pour décider quelles paires valent d’être testées. Je crée généralement chaque paire pour être différente dans un seul atsortingbut, plus un test qui teste deux objects équivalents.

J’ai aussi parfois une méthode checkInvariants 3 arguments, bien que je trouve que cela est moins utile pour trouver les défauts, donc je ne le fais pas souvent