Как следует юнит-тестировать контракт hashCode-equals?

79

В двух словах, контракт hashCode согласно объекту Java object.hashCode ():

  1. Хэш-код не должен меняться, если что-то, влияющее на equals (), не изменится.
  2. equals () подразумевает, что хеш-коды ==

Предположим, что интерес в первую очередь связан с неизменяемыми объектами данных - их информация никогда не меняется после создания, поэтому предполагается, что № 1 сохраняется. Остается №2: проблема просто в подтверждении того, что равенство подразумевает хэш-код ==.

Очевидно, что мы не можем протестировать каждый мыслимый объект данных, если этот набор не является тривиально маленьким. Итак, как лучше всего написать модульный тест, который может выявить распространенные случаи?

Поскольку экземпляры этого класса неизменяемы, существуют ограниченные способы создания такого объекта; этот модульный тест должен по возможности охватывать их все. Вне моей головы точки входа - это конструкторы, десериализация и конструкторы подклассов (которые должны быть сведены к проблеме вызова конструктора).

[Я попытаюсь ответить на свой вопрос путем исследования. Информация от других StackOverflowers - долгожданный механизм безопасности для этого процесса.]

[Это может быть применимо к другим языкам объектно-ориентированного программирования, поэтому я добавляю этот тег.]

Пол Бринкли
источник
1
Обычно я обнаруживаю, что контракт уже разорван из-за реализации equals или hashcode, но не из-за того и другого. Openpojo помогает в проекте Java. EqualsAndHashCodeMatchRule помогает в этом. Уже существующие ответы предоставляют достаточно подробностей относительно тестирования остальной части контракта.
Michiel Leegwater 01

Ответы:

73

EqualsVerifier - относительно новый проект с открытым исходным кодом, который отлично справляется с тестированием контракта на равенство. У него нет проблем, которые есть у EqualsTester от GSBase. Я определенно рекомендовал бы это.

Бенно Рихтерс
источник
7
Просто использование EqualsVerifier научило меня кое-чему о Java!
Дэвид
Я сумасшедший? EqualsVerifier, похоже, вообще не проверял семантику объекта значения! Он думает, что мой класс правильно реализует equals (), но мой класс использует поведение по умолчанию "==". Как это может быть?! Должно быть, мне не хватает чего-то простого.
JB Rainsberger
Ага! Если я не отменяю equals (), EqualsVerifier предполагает, что все в порядке !? Я не ожидал этого. Я бы ожидал того же поведения, что и при переопределении equals (), что и при переопределении equals (), чтобы сделать то же самое, что и super.equals (). Странно.
JB Rainsberger
7
@JBRainsberger Привет! Создатель EqualsVerifier здесь. Мне жаль, что это вас смущает. Re: не переопределять равенства: вы можете включить ожидаемое поведение с помощью "allFieldsShouldBeUsed ()". Это будет значение по умолчанию в следующей версии по указанным вами причинам. Re: идентичные экземпляры: я не думаю, что смогу вам помочь, не увидев пример кода. Не могли бы вы уточнить в новом вопросе или в списке рассылки EV на groups.google.com/forum/?fromgroups#!forum/equalsverifier ? Благодаря!
jqno 03
1
EqualsVerifier действительно мощный. Я выиграл огромное количество времени, не проверяя каждую потенциальную комбинацию не равных объектов. Сообщения об ошибках действительно точны и поучительны. И верификатор легко настраивается, если ваше соглашение о кодировании отличается от ожидаемого по умолчанию. Отличная работа @jqno
gontard 07
11

Я бы посоветовал подумать, почему и как это может не выполняться, а затем написать несколько модульных тестов, нацеленных на эти ситуации.

Например, допустим, у вас есть собственный Setкласс. Два набора равны, если они содержат одинаковые элементы, но могут отличаться базовые структуры данных двух равных наборов, если эти элементы хранятся в другом порядке. Например:

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

В этом случае порядок элементов в наборах может повлиять на их хэш, в зависимости от реализованного вами алгоритма хеширования. Я бы написал такой тест, поскольку он проверяет случай, когда я знаю, что какой-то алгоритм хеширования может выдать разные результаты для двух объектов, которые я определил как равные.

Вы должны использовать аналогичный стандарт со своим собственным классом, каким бы он ни был.

Эли Кортрайт
источник
6

Для этого стоит использовать аддоны junit. Ознакомьтесь с классом EqualsHashCodeTestCase http://junit-addons.sourceforge.net/, вы можете расширить его и реализовать createInstance и createNotEqualInstance, это позволит проверить правильность методов equals и hashCode.


источник
4

Я бы порекомендовал EqualsTester от GSBase. В основном он делает то, что вы хотите. Однако у меня есть две (незначительные) проблемы:

  • Конструктор выполняет всю работу, что я не считаю хорошей практикой.
  • Он не работает, когда экземпляр класса A равен экземпляру подкласса класса A. Это не обязательно является нарушением контракта равенства.
Бенно Рихтерс
источник
2

[На момент написания было опубликовано еще три ответа.]

Повторяю, цель моего вопроса - найти стандартные примеры тестов, чтобы подтвердить это hashCodeи equalsсогласиться друг с другом. Мой подход к этому вопросу состоит в том, чтобы представить общие пути, которые используют программисты при написании рассматриваемых классов, а именно неизменяемых данных. Например:

  1. Написал equals()без письма hashCode(). Это часто означает, что равенство определялось как равенство полей двух экземпляров.
  2. Написал hashCode()без письма equals(). Это может означать, что программист искал более эффективный алгоритм хеширования.

В случае №2 мне кажется, что проблема не существует. Никаких дополнительных экземпляров сделано не былоequals() , поэтому не требуется дополнительных экземпляров с одинаковыми хэш-кодами. В худшем случае алгоритм хеширования может дать более низкую производительность для хэш-карт, что выходит за рамки этого вопроса.

В случае №1 стандартный модульный тест влечет за собой создание двух экземпляров одного и того же объекта с одинаковыми данными, передаваемыми конструктору, и проверку одинаковых хэш-кодов. А как насчет ложных срабатываний? Можно выбрать параметры конструктора, которые просто дают одинаковые хэш-коды на все еще ненадежном алгоритме. Модульный тест, который стремится избегать таких параметров, отвечает духу этого вопроса. Ярлык здесь - проверить исходный код equals(), хорошенько подумать и написать тест на его основе, но хотя в некоторых случаях это может быть необходимо, также могут быть общие тесты, которые выявляют общие проблемы - и такие тесты также соответствуют духу этого вопроса.

Например, если у тестируемого класса (назовите его Data) есть конструктор, который принимает String, и экземпляры, построенные из Strings, которые дают equals()экземпляры, которые были equals(), тогда хороший тест, вероятно, будет тестировать:

  • new Data("foo")
  • еще один new Data("foo")

Мы могли бы даже проверить хэш-код new Data(new String("foo")), чтобы заставить String не интернироваться, хотя Data.equals(), на мой взгляд , это с большей вероятностью даст правильный хеш-код, чем правильный результат.

Ответ Эли Кортрайта - это пример того, как тщательно обдумывать способ взлома алгоритма хеширования, основанного на знании equalsспецификации. Хороший пример специальной коллекции, так как пользовательские Collections действительно появляются время от времени и весьма склонны к сбоям в алгоритме хеширования.

Пол Бринкли
источник
1

Это один из немногих случаев, когда у меня было бы несколько утверждений в тесте. Поскольку вам нужно протестировать метод equals, вы также должны одновременно проверить метод hashCode. Поэтому в каждом из ваших тестовых случаев метода equals также проверьте контракт 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());

И, конечно же, еще одно упражнение - проверка хорошего хэш-кода. Честно говоря, я бы не стал выполнять двойную проверку, чтобы убедиться, что hashCode не изменился в одном и том же прогоне, с подобными проблемами лучше справиться, поймав их в обзоре кода и помогая разработчику понять, почему это не лучший способ писать методы hashCode.

Роб Спилденнер
источник
0

Если у меня есть класс Thing, как и у большинства других, я пишу класс ThingTest, содержащий все модульные тесты для этого класса. У каждого ThingTestесть метод

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

и если Thingкласс переопределяет hashCode и равен ему, у него есть метод

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

Этот метод отвечает за проверку всех инвариантов, которые предназначены для удержания между любой парой Thingобъектов. ObjectTestМетод он делегирует отвечает за проверку всех инвариантов , которые должны удерживать между любой парой объектов. Как equalsи hashCodeметоды всех объектов, этот метод проверяет соответствие hashCodeи equalsсогласованность.

Затем у меня есть несколько тестовых методов, которые создают пары Thingобъектов и передают их парному checkInvariantsметоду. Я использую разделение по эквивалентности, чтобы решить, какие пары стоит тестировать. Я обычно создаю каждую пару так, чтобы она отличалась только одним атрибутом, плюс тест, который проверяет два эквивалентных объекта.

У меня также иногда есть checkInvariantsметод с тремя аргументами , хотя я считаю, что он менее полезен при обнаружении дефектов findinf, поэтому я делаю это не часто

Raedwald
источник
Это похоже на то, что здесь упоминает другой плакат: stackoverflow.com/a/190989/545127
Raedwald