Как один unit test hashCode-equals заключает контракт?

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

  • Хэш-код не должен изменяться, если только что-то не влияет на значения equals()
  • equals() означает, что хэш-коды == ==/

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

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

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

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

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

Ответ 1

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

Ответ 2

Моим советом было бы подумать, почему/как это может когда-либо быть недействительным, а затем написать некоторые модульные тесты, предназначенные для этих ситуаций.

Например, скажем, у вас был пользовательский класс 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() );

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

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

Ответ 3

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

Ответ 4

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

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

Ответ 5

[На момент написания этой статьи были опубликованы три других ответа.]

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

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

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

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

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

  • new Data("foo")
  • другой new Data("foo")

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

Ответ Eli Courtwright - это пример того, как трудно преодолеть хэш-алгоритм, основанный на знании спецификации equals. Пример специальной коллекции - хороший, так как пользовательские Collection иногда появляются и весьма склонны к ошибкам в хэш-алгоритме.

Ответ 6

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

Ответ 8

Если у меня есть класс 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, хотя я считаю, что это менее полезно для обнаружения дефектов, поэтому я часто этого не делаю