Как проверить на равенство сложных графов объектов? - PullRequest
23 голосов
/ 11 сентября 2009

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

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

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

    assertEquals(o1, o2);
}

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

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

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

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

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

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

Ответы [ 9 ]

10 голосов
/ 11 сентября 2009

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

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

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

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

Expected: is {singleBladed is true, color is PURPLE, hilt is {...}}  
but: is {color is GREEN}
8 голосов
/ 11 сентября 2009

То, что вы можете сделать, это визуализировать каждый объект в 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, указывающий расположение различных элементов.

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

4 голосов
/ 11 сентября 2009

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

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

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

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

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

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

(* Учтите, что шаг 3 в рецепте Джоша Блоха по написанию хорошего метода hashCode состоит в том, чтобы протестировать его, чтобы убедиться, что «равные» объекты имеют равные значения hashCode, и что вы охватили все возможные варианты. сам по себе не тривиален. Более тонким и даже более сложным для проверки является распределение)

3 голосов
/ 12 декабря 2012

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

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

Этот метод будет работать с циклическим графом объектов следующим образом: 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 () имеет обнаружение цикла и поэтому будет работать на ЛЮБОМ графе объектов.

1 голос
/ 11 сентября 2009

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

1 голос
/ 11 сентября 2009

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

0 голосов
/ 18 августа 2015

Если вы хотите, чтобы ваши тесты были написаны на языке 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

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

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

0 голосов
/ 11 сентября 2009

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

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

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

Делая это таким образом, с помощью утверждения JUnit для каждого сравнения, вы будете знать, где произошла ошибка.

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

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

0 голосов
/ 11 сентября 2009

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

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

НТН

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...