Wie sollte eine Einheit den hashCode-equals-Vertrag testen?

79

Kurz gesagt, der HashCode-Vertrag gemäß Javas object.hashCode ():

  1. Der Hash-Code sollte sich nur ändern, wenn sich etwas auf equals () auswirkt
  2. equals () impliziert, dass Hash-Codes == sind

Nehmen wir vor allem das Interesse an unveränderlichen Datenobjekten an - ihre Informationen ändern sich nach ihrer Erstellung nie mehr, daher wird angenommen, dass # 1 gilt. Damit bleibt # 2: Das Problem besteht einfach darin, zu bestätigen, dass gleich Hash-Code == impliziert.

Offensichtlich können wir nicht jedes denkbare Datenobjekt testen, es sei denn, diese Menge ist trivial klein. Was ist also der beste Weg, um einen Komponententest zu schreiben, der wahrscheinlich die häufigsten Fälle erfasst?

Da die Instanzen dieser Klasse unveränderlich sind, gibt es nur begrenzte Möglichkeiten, ein solches Objekt zu erstellen. Dieser Unit-Test sollte nach Möglichkeit alle abdecken. Die Einstiegspunkte sind die Konstruktoren, Deserialisierungen und Konstruktoren von Unterklassen (die auf das Konstruktoraufrufproblem reduziert werden sollten).

[Ich werde versuchen, meine eigene Frage durch Recherche zu beantworten. Die Eingabe von anderen StackOverflowers ist ein willkommener Sicherheitsmechanismus für diesen Prozess.]

[Dies könnte auf andere OO-Sprachen anwendbar sein, daher füge ich dieses Tag hinzu.]

Paul Brinkley
quelle
1
Normalerweise stelle ich fest, dass der Vertrag bereits aufgrund der Implementierung von Equals oder Hashcode gebrochen wurde, aber nicht beider. Dort hilft openpojo in einem Java-Projekt. Die EqualsAndHashCodeMatchRule hilft dabei. Die bereits vorhandenen Antworten enthalten genügend Details zum Testen des restlichen Vertrags.
Michiel Leegwater

Antworten:

73

EqualsVerifier ist ein relativ neues Open-Source-Projekt und leistet sehr gute Arbeit beim Testen des Equals-Vertrags. Es hat nicht die Probleme, die der EqualsTester von GSBase hat. Ich würde es auf jeden Fall empfehlen.

Benno Richters
quelle
7
Allein mit EqualsVerifier habe ich etwas über Java gelernt!
David
Bin ich verrückt? EqualsVerifier schien die Wertobjektsemantik überhaupt nicht zu überprüfen! Es wird angenommen, dass meine Klasse equals () korrekt implementiert, aber meine Klasse verwendet das Standardverhalten "==". Wie kann das sein?! Mir muss etwas Einfaches fehlen.
JB Rainsberger
Aha! Wenn ich equals () nicht überschreibe, geht EqualsVerifier davon aus, dass alles in Ordnung ist!? Das würde ich nicht erwarten. Ich würde erwarten, dass das gleiche Verhalten beim Nichtüberschreiben von equals () wie beim Überschreiben von equals () genau dasselbe tut, super.equals (). Seltsam.
JB Rainsberger
7
@JBRainsberger Hallo! Ersteller von EqualsVerifier hier. Es tut mir leid, dass Sie es verwirrend finden. Betreff: Nicht überschreiben gleich: Sie können Ihr erwartetes Verhalten mit "allFieldsShouldBeUsed ()" aktivieren. Dies ist aus den von Ihnen angegebenen Gründen die Standardeinstellung in der nächsten Version. Betreff: identische Instanzen: Ich glaube nicht, dass ich Ihnen helfen kann, ohne einen Beispielcode zu sehen. Können Sie bitte eine neue Frage oder die EV-Mailingliste unter groups.google.com/forum/?fromgroups#!forum/equalsverifier erläutern ? Vielen Dank!
jqno
1
EqualsVerifier ist wirklich mächtig. Ich habe viel Zeit gewonnen, indem ich nicht jede mögliche Kombination von Objekten getestet habe, die nicht gleich sind. Die Fehlermeldungen sind sehr präzise und lehrreich. Und der Verifizierer ist sehr konfigurierbar, wenn Ihre Codierungskonvention von der Standarderwartung abweicht. Großartige Arbeit @jqno
Pontard
11

Mein Rat wäre, darüber nachzudenken, warum / wie dies jemals nicht zutreffen könnte, und dann einige Komponententests zu schreiben, die auf diese Situationen abzielen.

Angenommen, Sie hatten eine benutzerdefinierte SetKlasse. Zwei Mengen sind gleich, wenn sie dieselben Elemente enthalten. Es ist jedoch möglich, dass sich die zugrunde liegenden Datenstrukturen von zwei gleichen Mengen unterscheiden, wenn diese Elemente in einer anderen Reihenfolge gespeichert werden. Zum Beispiel:

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

In diesem Fall kann sich die Reihenfolge der Elemente in den Mengen auf ihren Hash auswirken, abhängig von dem von Ihnen implementierten Hashing-Algorithmus. Dies ist also die Art von Test, die ich schreiben würde, da er den Fall testet, in dem ich weiß, dass ein Hashing-Algorithmus für zwei Objekte, die ich als gleich definiert habe, unterschiedliche Ergebnisse erzielen kann.

Sie sollten einen ähnlichen Standard mit Ihrer eigenen benutzerdefinierten Klasse verwenden, was auch immer das ist.

Eli Courtwright
quelle
6

Es lohnt sich, dafür die Junit-Addons zu verwenden. Schauen Sie sich die Klasse EqualsHashCodeTestCase http://junit-addons.sourceforge.net/ an. Sie können diese erweitern und createInstance und createNotEqualInstance implementieren. Dadurch wird überprüft, ob die Methoden equals und hashCode korrekt sind.


quelle
4

Ich würde den EqualsTester von GSBase empfehlen. Es macht im Grunde, was Sie wollen. Ich habe jedoch zwei (kleinere) Probleme damit:

  • Der Konstruktor erledigt die ganze Arbeit, die ich nicht als gute Praxis betrachte.
  • Es schlägt fehl, wenn eine Instanz der Klasse A einer Instanz einer Unterklasse der Klasse A entspricht. Dies ist nicht unbedingt eine Verletzung des Gleichstellungsvertrags.
Benno Richters
quelle
2

[Zum Zeitpunkt dieses Schreibens wurden drei weitere Antworten veröffentlicht.]

Um es noch einmal zu wiederholen, das Ziel meiner Frage ist es, Standardfälle von Tests zu finden, um dies zu bestätigen hashCodeund equalsmiteinander übereinzustimmen. Mein Ansatz für diese Frage besteht darin, mir die gemeinsamen Wege vorzustellen, die Programmierer beim Schreiben der fraglichen Klassen eingeschlagen haben, nämlich unveränderliche Daten. Zum Beispiel:

  1. Schrieb equals()ohne zu schreiben hashCode(). Dies bedeutet häufig, dass Gleichheit als Gleichheit der Felder zweier Instanzen definiert wurde.
  2. Schrieb hashCode()ohne zu schreiben equals(). Dies könnte bedeuten, dass der Programmierer nach einem effizienteren Hashing-Algorithmus suchte.

Im Fall von # 2 scheint mir das Problem nicht zu existieren. Es wurden keine zusätzlichen Instanzen erstellt equals(), daher sind keine zusätzlichen Instanzen erforderlich, um gleiche Hash-Codes zu haben. Im schlimmsten Fall kann der Hash-Algorithmus zu einer schlechteren Leistung für Hash-Maps führen, was außerhalb des Rahmens dieser Frage liegt.

Im Fall von # 1 umfasst der Standard-Komponententest das Erstellen von zwei Instanzen desselben Objekts mit denselben Daten, die an den Konstruktor übergeben werden, und das Überprüfen gleicher Hash-Codes. Was ist mit Fehlalarmen? Es ist möglich, Konstruktorparameter auszuwählen, die zufällig gleiche Hash-Codes auf einem dennoch unsoliden Algorithmus ergeben. Ein Unit-Test, der dazu neigt, solche Parameter zu vermeiden, würde den Geist dieser Frage erfüllen. Die Abkürzung hier besteht darin, den Quellcode zu überprüfen, gründlich equals()nachzudenken und einen darauf basierenden Test zu schreiben. In einigen Fällen kann dies zwar erforderlich sein, es kann jedoch auch allgemeine Tests geben, die häufig auftretende Probleme auffangen - und solche Tests erfüllen auch den Geist dieser Frage.

Wenn die zu testende Klasse (nennen Sie sie Daten) beispielsweise einen Konstruktor hat, der einen String akzeptiert, und Instanzen, die aus Strings erstellt wurden und Instanzen equals()ergeben, die es waren equals(), würde ein guter Test wahrscheinlich Folgendes testen:

  • new Data("foo")
  • Ein weiterer new Data("foo")

Wir könnten sogar den Hash-Code überprüfen new Data(new String("foo")), um zu erzwingen, dass der String nicht interniert wird, obwohl dies Data.equals()meiner Meinung nach eher zu einem korrekten Hash-Code als zu einem korrekten Ergebnis führt.

Die Antwort von Eli Courtwright ist ein Beispiel dafür, wie man überlegt, wie man den Hash-Algorithmus auf der Grundlage der Kenntnis der equalsSpezifikation brechen kann . Das Beispiel einer speziellen Sammlung ist gut, da benutzerdefinierte Collections manchmal auftauchen und im Hash-Algorithmus sehr anfällig für Fehler sind.

Paul Brinkley
quelle
1

Dies ist einer der wenigen Fälle, in denen ich in einem Test mehrere Asserts hätte. Da Sie die Methode equals testen müssen, sollten Sie gleichzeitig auch die Methode hashCode überprüfen. Überprüfen Sie daher in jedem Testfall mit gleicher Methode auch den HashCode-Vertrag.

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());

Und natürlich ist es eine weitere Übung, nach einem guten Hashcode zu suchen. Ehrlich gesagt würde ich mir nicht die Mühe machen, die doppelte Überprüfung durchzuführen, um sicherzustellen, dass sich der HashCode nicht im selben Lauf ändert. Diese Art von Problem lässt sich besser lösen, indem ich es in einer Codeüberprüfung abfange und dem Entwickler helfe, zu verstehen, warum dies kein guter Weg ist HashCode-Methoden schreiben.

Rob Spieldenner
quelle
0

Wenn ich eine Klasse habe Thing, schreibe ich wie die meisten anderen eine Klasse ThingTest, die alle Komponententests für diese Klasse enthält. Jeder ThingTesthat eine Methode

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

und wenn die ThingKlasse hashCode überschreibt und gleich ist, hat sie eine Methode

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

Diese Methode ist dafür verantwortlich, alle Invarianten zu überprüfen , die zwischen zwei Objektpaaren gespeichert werden sollen Thing. Die ObjectTestMethode, an die delegiert wird, ist für die Überprüfung aller Invarianten verantwortlich, die zwischen zwei Objektpaaren enthalten sein müssen. Da equalsund hashCodeMethoden aller Objekte sind, überprüft diese Methode dies hashCodeund equalsist konsistent.

Ich habe dann einige Testmethoden, die Paare von ThingObjekten erstellen und diese an die paarweise checkInvariantsMethode übergeben. Ich verwende die Äquivalenzpartitionierung, um zu entscheiden, welche Paare es wert sind, getestet zu werden. Normalerweise erstelle ich jedes Paar so, dass es nur in einem Attribut unterschiedlich ist, plus einem Test, der zwei äquivalente Objekte testet.

Ich habe auch manchmal eine 3-Argument- checkInvariantsMethode, obwohl ich finde, dass dies bei Findinf-Fehlern weniger nützlich ist, deshalb mache ich das nicht oft

Raedwald
quelle
Dies ähnelt dem, was ein anderes Poster hier erwähnt: stackoverflow.com/a/190989/545127
Raedwald