Jak jedna jednostka powinna przetestować kontrakt hashCode-equals?

79

Krótko mówiąc, kontrakt hashCode, zgodnie z object.hashCode () Javy:

  1. Kod skrótu nie powinien się zmieniać, chyba że zmieni się coś, co ma wpływ na equals ()
  2. equals () oznacza, że ​​kody skrótu to ==

Załóżmy, że interesują nas przede wszystkim niezmienne obiekty danych - ich informacje nigdy się nie zmieniają po ich skonstruowaniu, więc zakłada się, że # 1 jest aktualne. To pozostawia # 2: problem polega po prostu na potwierdzeniu, że równa się implikuje kod skrótu ==.

Oczywiście nie możemy przetestować każdego wyobrażalnego obiektu danych, chyba że ten zestaw jest banalnie mały. Jaki jest więc najlepszy sposób napisania testu jednostkowego, który prawdopodobnie wyłapie typowe przypadki?

Ponieważ instancje tej klasy są niezmienne, istnieją ograniczone sposoby tworzenia takiego obiektu; ten test jednostkowy powinien obejmować je wszystkie, jeśli to możliwe. Na początek punktami wejścia są konstruktory, deserializacja i konstruktory podklas (które powinny być zredukowane do problemu wywołania konstruktora).

[Spróbuję odpowiedzieć na swoje pytanie poprzez badania. Dane wejściowe z innych StackOverflowers to pożądany mechanizm bezpieczeństwa w tym procesie.]

[Może to dotyczyć innych języków OO, więc dodaję ten tag.]

Paul Brinkley
źródło
1
Zwykle stwierdzam, że umowa już się zepsuła z powodu implementacji equals lub hashcode, ale nie obu. Tam openpojo pomaga w projekcie Java. Pomaga w tym EqualsAndHashCodeMatchRule. Już istniejące odpowiedzi zawierają wystarczająco dużo szczegółów dotyczących testowania pozostałej części zamówienia.
Michiel Leegwater

Odpowiedzi:

73

EqualsVerifier to stosunkowo nowy projekt o otwartym kodzie źródłowym, który bardzo dobrze sprawdza się przy testowaniu umowy równości. Nie ma problemów, które ma EqualsTester z GSBase. Zdecydowanie poleciłbym to.

Benno Richters
źródło
7
Samo użycie EqualsVerifier nauczyło mnie czegoś o Javie!
David
Czy jestem szalony? EqualsVerifier w ogóle nie sprawdzał semantyki obiektu wartości! Uważa, że ​​moja klasa poprawnie implementuje equals (), ale moja klasa używa domyślnego zachowania „==”. Jak to może być?! Chyba brakuje mi czegoś prostego.
JB Rainsberger
Aha! Jeśli nie zastąpię równa się (), EqualsVerifier przyjmie, że wszystko jest w porządku !? Nie tego bym się spodziewał. Spodziewałbym się takiego samego zachowania, jeśli nie przesłaniamy equals (), jak w przypadku przesłaniania equals (), aby zrobić dokładnie to samo super.equals (). Dziwne.
JB Rainsberger
7
@JBRainsberger Hi! Twórca EqualsVerifier tutaj. Przepraszam, że to mylące. Re: not overriding equals: możesz włączyć oczekiwane zachowanie za pomocą „allFieldsShouldBeUsed ()”. Będzie to ustawienie domyślne w następnej wersji z powodów, które podasz. Re: identyczne instancje: Nie sądzę, żebym mógł Ci pomóc bez zobaczenia przykładowego kodu. Czy możesz je rozwinąć w nowym pytaniu lub na liście mailingowej EV na groups.google.com/forum/?fromgroups#!forum/equalsverifier ? Dzięki!
jqno
1
EqualsVerifier jest naprawdę potężny. Zyskałem ogromną ilość czasu, nie testując każdej potencjalnej kombinacji obiektów, które nie są równe. Komunikaty o błędach są naprawdę dokładne i pouczające. Weryfikator jest bardzo konfigurowalny, jeśli Twoja konwencja kodowania różni się od domyślnych oczekiwań. Świetna robota @jqno
gontard
11

Radziłbym zastanowić się, dlaczego / jak może to nigdy nie być prawdą, a następnie napisać kilka testów jednostkowych, które dotyczą takich sytuacji.

Na przykład, powiedzmy, że masz Setklasę niestandardową . Dwa zestawy są równe, jeśli zawierają te same elementy, ale możliwe jest, że podstawowe struktury danych dwóch równych zestawów będą się różnić, jeśli te elementy są przechowywane w innej kolejności. Na przykład:

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

W takim przypadku kolejność elementów w zestawach może wpływać na ich hash, w zależności od zaimplementowanego algorytmu haszowania. To jest rodzaj testu, który bym napisał, ponieważ testuje przypadek, w którym wiem, że jakiś algorytm haszujący mógłby wygenerować różne wyniki dla dwóch obiektów, które zdefiniowałem jako równe.

Powinieneś użyć podobnego standardu z własną klasą niestandardową, cokolwiek to jest.

Eli Courtwright
źródło
6

Warto do tego wykorzystać dodatki junit. Sprawdź klasę EqualsHashCodeTestCase http://junit-addons.sourceforge.net/ możesz ją rozszerzyć i zaimplementować createInstance oraz createNotEqualInstance, to sprawdzi, czy metody equals i hashCode są poprawne.


źródło
4

Polecam EqualsTester z GSBase. Robi w zasadzie to, co chcesz. Mam z tym jednak dwa (drobne) problemy:

  • Konstruktor wykonuje całą pracę, której nie uważam za dobrą praktykę.
  • Błąd kończy się niepowodzeniem, gdy instancja klasy A jest równa instancji podklasy klasy A. Nie jest to koniecznie naruszeniem umowy równości.
Benno Richters
źródło
2

[W czasie pisania tego tekstu opublikowano trzy inne odpowiedzi.]

Powtarzam, celem mojego pytania jest znalezienie standardowych przypadków testów, które to potwierdzą hashCodei equalszgadzają się ze sobą. Moje podejście do tego pytania polega na wyobrażeniu sobie wspólnych ścieżek, którymi kierują się programiści pisząc omawiane klasy, a mianowicie niezmiennych danych. Na przykład:

  1. Napisałem equals()bez pisania hashCode(). To często oznacza, że ​​równość była definiowana jako równość pól dwóch instancji.
  2. Napisałem hashCode()bez pisania equals(). Może to oznaczać, że programista szukał wydajniejszego algorytmu mieszającego.

W przypadku # 2 wydaje mi się, że problem nie istnieje. Nie utworzono equals()żadnych dodatkowych instancji, więc żadne dodatkowe instancje nie muszą mieć równych kodów skrótów. W najgorszym przypadku algorytm wyznaczania wartości skrótu może dawać gorszą wydajność w przypadku map skrótów, co jest poza zakresem tego pytania.

W przypadku # 1 standardowy test jednostkowy polega na utworzeniu dwóch wystąpień tego samego obiektu z tymi samymi danymi przekazanymi do konstruktora i sprawdzeniu równych kodów skrótów. A co z fałszywymi alarmami? Możliwe jest wybranie parametrów konstruktora, które po prostu dają równe kody skrótu w mimo to niesprawnym algorytmie. Test jednostkowy, który ma tendencję do unikania takich parametrów, byłby zgodny z duchem tego pytania. Skrótem tutaj jest equals()zbadanie kodu źródłowego , przemyślenie i napisanie testu opartego na tym, ale chociaż może to być konieczne w niektórych przypadkach, mogą również istnieć popularne testy, które wychwytują typowe problemy - i takie testy również spełniają ducha tego pytania.

Na przykład, jeśli testowana klasa (nazwij ją Data) ma konstruktor, który pobiera String i instancje zbudowane z ciągów, które są equals()zwracanymi instancjami, które były equals(), wtedy dobry test prawdopodobnie przetestowałby:

  • new Data("foo")
  • inne new Data("foo")

Moglibyśmy nawet sprawdzić kod skrótu new Data(new String("foo")), aby wymusić na String, aby nie był internowany, chociaż Data.equals()moim zdaniem jest to bardziej prawdopodobne, że dostarczy poprawny kod skrótu, niż daje poprawny wynik.

Odpowiedź Eli Courtwright jest przykładem intensywnego zastanawiania się nad sposobem złamania algorytmu wyznaczania wartości skrótu w oparciu o znajomość equalsspecyfikacji. Przykład specjalnej kolekcji jest dobry, ponieważ Collectionczasami pojawiają się pliki utworzone przez użytkownika i są one dość podatne na muckupy w algorytmie haszującym.

Paul Brinkley
źródło
1

To jeden z niewielu przypadków, w których w teście miałbym wiele potwierdzeń. Ponieważ musisz przetestować metodę equals, powinieneś również sprawdzić metodę hashCode w tym samym czasie. Więc w każdym przypadku testowym metody równości sprawdź również kontrakt 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());

I oczywiście sprawdzenie dobrego hashCode to kolejne ćwiczenie. Szczerze mówiąc, nie zawracałbym sobie głowy podwójnym sprawdzaniem, aby upewnić się, że hashCode nie zmienia się w tym samym przebiegu, tego rodzaju problem można lepiej rozwiązać, przechwytując go w przeglądzie kodu i pomagając deweloperowi zrozumieć, dlaczego to nie jest dobry sposób do pisania metod hashCode.

Rob Spieldenner
źródło
0

Jeśli mam klasę Thing, tak jak większość innych, piszę klasę ThingTest, która zawiera wszystkie testy jednostkowe dla tej klasy. Każdy ThingTestma swoją metodę

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

a jeśli Thingklasa przesłania hashCode i jest równa, to ma metodę

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

Ta metoda jest odpowiedzialna za sprawdzanie wszystkich niezmienników, które są przeznaczone do przechowywania między dowolnymi parami Thingobiektów. ObjectTestMetodzie delegatów jest odpowiedzialny za sprawdzenie wszystkich niezmienników, że musi posiadać między dowolnymi dwoma obiektami. Ponieważ equalsi hashCodesą metodami wszystkich obiektów, metoda ta sprawdza to hashCodei equalsjest spójna.

Następnie mam kilka metod testowych, które tworzą pary Thingobiektów i przekazują je do checkInvariantsmetody parami . Używam podziału równoważności, aby zdecydować, które pary warto przetestować. Zwykle tworzę każdą parę tak, aby różniła się tylko jednym atrybutem oraz testem, który testuje dwa równoważne obiekty.

Czasami mam też checkInvariantsmetodę 3-argumentową , chociaż uważam, że jest to mniej przydatne w przypadku defektów findinf, więc nie robię tego często

Raedwald
źródło
Jest to podobne do tego, o czym wspomina inny plakat tutaj: stackoverflow.com/a/190989/545127
Raedwald.