2009-09-11 7 views
22

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

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

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

    assertEquals(o1, o2); 
} 

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

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

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

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

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

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

+2

+1 хорошо, что вы справляетесь с этим. Я хотел бы посмотреть, какие решения нашли другие люди. – KLE

ответ

8

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

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

+0

+1 Мне нравится этот ... аналогичный подход к сравнению 'toString()', не требуя 'toString()'. Я подозреваю, что придерживаться сравнения строк и поддержки IDE было бы проще. – skaffman

+1

Я думаю, вы могли бы легко написать метод утилиты, называемый assertSameDeeply() или похожий, и он будет полностью общим. Просто статически импортируйте его и используйте его, как и все другие вещи JUnit. –

+0

Мне нравится это решение, потому что результаты сравнения хорошо отформатированы. Тем не менее, я считаю, что указатель матового b на Hamcrest очень хорош, и решение лучше пахнет. – Kariem

1

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

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

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


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

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

  • ползание через все свойства объектов; остановка по адресу:

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

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

С этого Обходом суперкласса, подклассы сделаны для многих нужд:

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

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

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

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

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

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

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

1

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

+3

Я не согласен, это не всегда практично. Например, я создаю объект JPA, сохраняю его, а затем извлекаю, и я хочу проверить, что полученный объект равен тому, который я сохранил. Я могу сделать это только для объектов верхнего уровня. – skaffman

+3

Дело в том, что он проверяет, что 2 объекта одинаковы (одно сравнение). Эти объекты могут быть сложными, и хотя утверждение тривиально и правильно, диагноз проблем, когда они отличаются, не является. –

+3

Его единственная вещь - «эти объекты равны» –

0

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

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

НТН

0

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

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

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

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

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

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

10

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

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

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

Hamcrest даст вам обратно выход, как это (в котором только поля, которые отличаются показаны):

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

+1 спасибо за ссылки – skaffman

4

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

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

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

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

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

Причина, по которой я делаю это, это то, что writing a good hashCode isn't trivial, и требует тестирования (*), в то время как использование в String исключает тестирование.

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

+0

Как вы имеете дело со структурами, где порядок подэлементов не гарантируется. Карты или коллекции, например. –

+1

@ EldarBudagov Помните, что я написал свой ответ в 2009 году и что есть, возможно, лучшие решения (xml, deepcopy и т. Д.), Доступные сейчас. Но мой подход состоит в том, чтобы заставить записи в порядке (используя естественный порядок ключей, например, на карте), помня о кешировании строчной версии. Помните также, что этот подход работает только с объектами, которые либо предотвращают модификацию, либо полностью контролируют их подобъекты, позволяя знать объекты верхнего уровня, когда записи добавляются или удаляются. – CPerkins

3

код для этой задачи существует в http://code.google.com/p/deep-equals/

Используйте DeepEquals.deepEquals (а, б) для сравнения двух объектов 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() имеет обнаружение цикла и, следовательно, будет работать с любым графиком объекта.

+0

Можно ли увидеть, в чем разница, если объект не совпадает? Также можно исключить некоторые поля из сравнения? – tuk

0

Если вы хотите, чтобы ваши тесты были написаны на scala, вы можете использовать matchete. Это коллекция matchers, которые могут быть использованы с JUnit и обеспечить, среди прочего, способность к compare objects graphs:

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.

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

Смежные вопросы