web-dev-qa-db-fra.com

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

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

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

Supposons que nous nous intéressions principalement aux objets de données immuables - leurs informations ne changent jamais après leur construction, donc # 1 est supposé tenir. Cela laisse # 2: le problème est simplement de confirmer que l'égalité implique un code de hachage ==.

De toute évidence, nous ne pouvons pas tester tous les objets de données imaginables à moins que cet ensemble soit trivialement petit. Alors, quelle est la meilleure façon d'écrire un test unitaire susceptible de détecter les cas courants?

Étant donné que les instances de cette classe sont immuables, il existe des moyens limités de construire un tel objet; si possible, ce test unitaire devrait les couvrir tous. Au sommet 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 via la recherche. L'apport d'autres StackOverflowers est un mécanisme de sécurité bienvenu dans ce processus.]

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

72
Paul Brinkley

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

69
Benno Richters

Mon conseil serait de penser pourquoi/comment cela pourrait ne jamais se vérifier, puis d'écrire des tests unitaires qui ciblent ces situations.

Par exemple, supposons que vous disposiez d'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 String[]{"Hello", "World"} );
MySet s2 = new MySet( new String[]{"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, selon l'algorithme de hachage que vous avez implémenté. C'est donc le genre de test que j'écrirais, 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 objets que j'ai définis comme égaux.

Vous devez utiliser un standard similaire avec votre propre classe personnalisée, quelle qu'elle soit.

11
Eli Courtwright

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

6
amc

Je recommanderais le EqualsTester de GSBase. Il fait essentiellement ce que vous voulez. J'ai cependant deux (petits) problèmes avec:

  • 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 classe A. Ce n'est pas nécessairement une violation du contrat égal.
4
Benno Richters

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

Pour le répéter, le but de ma question est de trouver des cas standard de tests pour confirmer que hashCode et equals sont d'accord l'un avec l'autre. Mon approche de cette question est d'imaginer les chemins communs empruntés 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. A écrit hashCode() sans écrire equals(). Cela peut signifier que le programmeur recherchait un algorithme de hachage plus efficace.

Dans le cas de # 2, le problème me semble inexistant. Aucune instance supplémentaire n'a été créée equals(), donc aucune instance supplémentaire n'est requise pour avoir des codes de hachage égaux. Au pire, l'algorithme de hachage peut produire de moins bonnes performances pour les cartes de hachage, ce qui est en dehors de la portée de cette question.

Dans le cas de # 1, le test unitaire standard implique la création de deux instances du même objet avec les mêmes données transmises au constructeur et la vérification des codes de hachage égaux. Et les faux positifs? Il est possible de choisir des paramètres de constructeur qui produisent des codes de hachage égaux sur un algorithme néanmoins peu solide. Un test unitaire qui tend à éviter de tels paramètres répondrait à l'esprit de cette question. Le raccourci ici consiste à inspecter le code source pour equals(), à réfléchir et à écrire un test basé sur cela, mais bien que cela puisse être nécessaire dans certains cas, il peut également y avoir des tests communs qui détectent les problèmes courants - et ces tests remplissent également l'esprit de cette question.

Par exemple, si la classe à tester (appelez-la Data) a un constructeur qui prend une chaîne et des instances construites à partir de chaînes qui sont equals() ont produit des instances qui étaient equals(), alors un un bon test testerait probablement:

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

Nous pourrions même vérifier le code de hachage pour new Data(new String("foo")), pour forcer la chaîne à ne pas être internée, bien que cela soit plus susceptible de produire un code de hachage correct que Data.equals() ne donne un résultat correct, À mon avis.

La réponse d'Eli Courtwright est un exemple de réflexion sérieuse sur un moyen de casser l'algorithme de hachage basé sur la connaissance de la spécification equals. L'exemple d'une collection spéciale en est un bon, car les Collection créés par l'utilisateur apparaissent parfois et sont assez sujets aux muckups dans l'algorithme de hachage.

2
Paul Brinkley

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 equals et hashCode.

1
Mangoose

C'est l'un des seuls cas où j'aurais plusieurs assertions dans un test. Puisque vous devez tester la méthode equals, vous devez également vérifier la méthode hashCode en même temps. Donc, sur chacun de vos cas de test de 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 vous assurer que le hashCode ne change pas dans le même temps, ce genre de problème est mieux géré en l'attrapant dans une revue de code et en aidant le développeur à comprendre pourquoi ce n'est pas une bonne façon pour écrire des méthodes hashCode.

1
Rob Spieldenner

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

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

et si la classe Thing remplace hashCode et est égale à elle a 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 responsable de la vérification des invariants tous conçus pour tenir entre n'importe quelle paire d'objets Thing. La méthode ObjectTest à laquelle elle délègue est responsable de la vérification de tous les invariants qui doivent tenir entre n'importe quelle paire d'objets. Comme equals et hashCode sont des méthodes de tous les objets, cette méthode vérifie que hashCode et equals sont cohérents.

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

J'ai aussi parfois une méthode à 3 arguments checkInvariants, même si je trouve que c'est moins utile dans les défauts findinf, donc je ne le fais pas souvent

0
Raedwald