Подтвердить что ты не робот

Как проверить равенство графов сложных объектов?

Скажем, у меня есть unit test, который хочет сравнить два комплекса для объектов для равенства. Объекты содержат много других глубоко вложенных объектов. Все классы объектов правильно определили методы equals().

Это не сложно:

@Test
public void objectEquality() {
    Object o1 = ...
    Object o2 = ...

    assertEquals(o1, o2);
}

Проблема в том, что если объекты не равны, все, что вы получаете, это сбой, без указания того, какая часть графика объекта не соответствует. Отладка этого может быть болезненным и разочаровывающим.

Мой текущий подход заключается в том, чтобы убедиться, что все реализует toString(), а затем сравнивается для равенства следующим образом:

    assertEquals(o1.toString(), o2.toString());

Это облегчает отслеживание сбоев тестирования, поскольку IDE, такие как Eclipse, имеют специальный визуальный компаратор для отображения различий строк в неудачных тестах. По существу, графы объектов представлены в текстовом виде, поэтому вы можете видеть, где разница. Пока toString() хорошо написан, он отлично работает.

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

Я ищу идеи для лучшего способа сравнения сложных графиков объектов. Любые мысли?

4b9b3361

Ответ 1

Что вы можете сделать, так это сделать каждый объект XML с помощью XStream, а затем использовать XMLUnit, чтобы выполнить сравнение в XML. Если они отличаются друг от друга, то вы получите контекстуальную информацию (в виде XPath, IIRC), сообщающую вам, где объекты отличаются.

например. из документа XMLUnit:

Comparing test xml to control xml [different] 
Expected element tag name 'uuid' but was 'localId' - 
comparing <uuid...> at /msg[1]/uuid[1] to <localId...> at /msg[1]/localId[1]

Обратите внимание на XPath, указывающий расположение разных элементов.

Вероятно, не быстро, но это может быть не проблемой для модульных тестов.

Ответ 2

Блог разработчиков Atlassian содержал несколько статей по этому самому вопросу, и как библиотека Hamcrest может отлаживать такой отказ теста очень очень просто:

В принципе, для такого утверждения:

assertThat(lukesFirstLightsaber, is(equalTo(maceWindusLightsaber)));

Hamcrest вернет вам результат, подобный этому (в котором показаны только разные поля):

Expected: is {singleBladed is true, color is PURPLE, hilt is {...}}  
but: is {color is GREEN}

Ответ 3

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

При разработке сложного объекта, для которого мне нужно написать метод equals (и, следовательно, метод hashCode), я склонен писать средство визуализации строк и использовать методы класса String и hashCode.

Средство визуализации, конечно же, не toString: на самом деле для людей не должно быть легко читать и включает все и только значения, которые мне нужно сравнить, и по привычке я помещаю их в том порядке, который контролирует как я хочу, чтобы они сортировались; ни одно из которых не обязательно верно для метода toString.

Естественно, я кэширую эту визуализированную строку (и значение hashCode). Он обычно закрыт, но оставляя кэшированную строку package-private, позволит вам увидеть это из ваших модульных тестов.

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

Причина, по которой я делаю это, заключается в том, что запись хорошего хэш-кода не является тривиальным и требует тестирования (*), один в String избегает тестирования.

(* Учтите, что шаг 3 в рецепте Джоша Блоха для написания хорошего метода hashCode заключается в том, чтобы проверить его, чтобы убедиться, что "равные" объекты имеют равные значения hashCode, и убедитесь, что вы охватили все возможные варианты. isn 't тривиальный сам по себе. Более тонкий и еще более трудный тест - это распределение)

Ответ 4

Код для этой проблемы существует в http://code.google.com/p/deep-equals/

Используйте DeepEquals.deepEquals(a, b) для сравнения двух объектов Java для семантического равенства. Это будет сравнивать объекты, используя любые собственные методы equals(), которые они могут иметь (если они имеют метод equals(), реализованный иначе, чем Object.equals()). Если нет, тогда этот метод будет рекурсивно сравнивать поле объектов по полю. По мере того как каждое поле встречается, оно будет пытаться использовать полученные equals(), если оно существует, в противном случае оно будет продолжать рекурсивно далее.

Этот метод будет работать над циклическим графом Object следующим образом: A- > B- > C- > A. У этого есть обнаружение цикла, так что ЛЮБОЙ два объекта можно сравнить, и он никогда не войдет в бесконечный цикл.

Используйте метод DeepEquals.hashCode(obj) для вычисления hashCode() для любого объекта. Подобно deepEquals(), он попытается вызвать метод hashCode(), если будет реализован пользовательский метод hashCode() (ниже Object.hashCode()), иначе он будет вычислять поле hashCode по полю, рекурсивно (Deep). Также как deepEquals(), этот метод будет обрабатывать графики объектов с циклами. Например, A- > B- > C- > A. В этом случае hashCode (A) == hashCode (B) == hashCode (C). DeepEquals.deepHashCode() имеет обнаружение цикла и, следовательно, будет работать с любым графиком объекта.

Ответ 5

Я следил за тем же треком, на котором вы находитесь. У меня также были дополнительные проблемы:

  • мы не можем изменять классы (для equals или toString), которыми мы не владеем (JDK), массивы и т.д.
  • Равенство иногда отличается в разных контекстах.

Например, равенство идентификаторов отслеживания может основываться на идентификаторах базы данных, когда они доступны (концепция "той же строки" ), полагаются на равенство некоторых полей (бизнес-ключ) (для несохраненных объектов). Для утверждения Junit вам может понадобиться равенство всех полей.


Итак, я создал объекты, которые проходят через график, выполняя свою работу, когда они идут.

Обычно существует суперкласс Обход:

  • сканировать все свойства объектов; остановка:

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

    • когда методы mustStopCurrent() или mustStopCompletely() возвращают true,
    • когда вы встречаете некоторые аннотации на геттере или классе,
    • когда текущий (класс, getter) принадлежит списку исключений
    • ...

Из этого суперкласса Crawling подклассы создаются для многих потребностей:

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

Вернуться к вопросу: Эти эквалайзеры могли запомнить путь к различным значениям, что было бы очень полезно для вашего разума JUnit, чтобы понять разницу.

  • Для создания Заказчиков. Например, сохранение объектов должно выполняться в определенном порядке, и эффективность будет диктовать, что сохранение одних и тех же классов даст огромный толчок.
  • Для сбора набора объектов, которые могут быть найдены на разных уровнях графика. Таким образом, петля по результату Collector очень проста.

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

Например, если бизнес-ключ в одном или нескольких полях определен в Hibernate с помощью @UniqueConstraint в классе, допустим, что все мои объекты имеют свойство getIdent(), реализованное в общем суперклассе. У моих сущностей суперкласса есть реализация по умолчанию этих 4 методов, которые полагаются на это знание, например (нули нужно позаботиться):

  • toString() печатает "myClass (key1 = value1, key2 = value2)"
  • hashCode() - "value1.hashCode() ^ value2.hashCode()"
  • equals() - "value1.equals(other.value1) && value2.equals(other.value2)"
  • compareTo() объединяет сравнение класса, value1 и value2.

Для объектов, производительность которых вызывает беспокойство, я просто переопределяю эти методы, чтобы не использовать рефлексию. Я могу проверить в регрессионных тестах JUnit, что две реализации ведут себя одинаково.

Ответ 6

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

Ответ 7

Мы используем библиотеку junitx для проверки равноценного контракта на всех наших "общих" объектах: http://www.extreme-java.de/junitx/

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

НТН

Ответ 8

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

Мне кажется, что ваш "единичный" тест не изолирует тестируемое устройство. Если, например, ваш граф объекта A-->B-->C и вы тестируете A, ваш unit test для A не должен заботиться о том, что работает метод equals() в C. Ваш unit test для C будет убедиться, что он работает.

Итак, я проверил бы следующее в тесте для метода A equals():  - сравнить два объекта A, которые имеют идентичные B, в обоих направлениях, например. a1.equals(a2) и a2.equals(a1).  - сравнить два объекта A, которые имеют разные B, в обоих направлениях

Таким образом, при утверждении JUnit для каждого сравнения вы узнаете, где произошел сбой.

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

Одна морщина может быть, если вы сравниваете коллекции. В этом случае я бы использовал утилиту для сравнения коллекций, например коллекций коллекций CollectionUtils.isEqualCollection(). Конечно, только для коллекций в тестируемом блоке.

Ответ 9

Если вы хотите, чтобы ваши тесты были написаны в scala, вы могли бы использовать matchete. Это набор совпадений, которые можно использовать с JUnit и, помимо прочего, обеспечивают возможность сравнить графики объектов:

case class Person(name: String, age: Int, address: Address)
case class Address(street: String)

Person("john",12, Address("rue de la paix")) must_== Person("john",12,Address("rue du bourg"))

Появится следующее сообщение об ошибке

org.junit.ComparisonFailure: Person(john,12,Address(street)) is not equal to Person(john,12,Address(different street))
Got      : address.street = 'rue de la paix'
Expected : address.street = 'rue du bourg'

Как вы можете видеть здесь, я использовал классы case, которые распознаются с помощью matchete, чтобы погрузиться в граф объектов. Это делается с помощью класса типа Diffable. Я не собираюсь обсуждать типы классов здесь, поэтому скажем, что это краеугольный камень для этого механизма, который сравнивает 2 экземпляра данного типа. Типы, которые не являются case-классами (поэтому в основном все типы в Java) получают по умолчанию Diffable, который использует equals. Это не очень полезно, если вы не предоставили Diffable для вашего конкретного типа:

// your java object
public class Person {
   public String name;
   public Address address;
}
// you scala test code
implicit val personDiffable : Diffable[Person] = Diffable.forFields(_.name,_.address)

// there you go you can now compare two person exactly the way you did it
// with the case classes

Итак, мы видели, что матет хорошо работает с базой java-кода. На самом деле я использую matchete на моей последней работе над большим проектом Java.

Отказ от ответственности: я автор макета:)