Проверка на глубокое равенство в тестах JUnit

#java #junit #equality

#java #junit #равенство

Вопрос:

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

Моей первой идеей было вручную реализовать метод equals во всех классах и использовать assertEquals. Я отказался от этого подхода, решив, что переопределение equals для выполнения глубокого сравнения с изменяемыми объектами — это плохо, поскольку вы почти всегда хотите, чтобы коллекции использовали ссылочное равенство для изменяемых объектов, которые они содержат [1].

Затем я подумал, что мог бы просто переименовать метод в contentEquals или что-то в этомроде. Однако, поразмыслив больше, я понял, что это не поможет мне найти регрессии того типа, который я искал. Если программист добавляет новое (изменяемое) поле и забывает добавить его в метод clone, то он, вероятно, забудет добавить его и в метод contentEquals, и все эти регрессионные тесты, которые я пишу, будут бесполезны.

Затем я написал отличную функцию assertContentEquals, которая использует отражение для проверки значения всех (непереходных) членов объекта, рекурсивно, если необходимо. Это позволяет избежать проблем с описанным выше методом сравнения вручную, поскольку по умолчанию предполагается, что все поля должны быть сохранены, и программист должен явно объявить поля для пропуска. Однако существуют законные случаи, когда поле действительно не должно быть одинаковым после клонирования [2]. Я ввел дополнительный параметр в Assertcontentequals, который перечисляет, какие поля следует игнорировать, но поскольку этот список объявлен в модульном тестировании, он очень быстро становится уродливым в случае рекурсивной проверки.

Итак, сейчас я подумываю вернуться к включению метода contentEquals в каждый тестируемый класс, но на этот раз реализованного с использованием вспомогательной функции, подобной assertContentsEquals, описанной выше. Таким образом, при рекурсивной работе исключения будут определены в каждом отдельном классе.

Есть комментарии? Как вы подходили к этой проблеме в прошлом?

Отредактировано, чтобы изложить мои мысли:

[1] Я получил обоснование для того, чтобы не переопределять equals в изменяемых классах из этой статьи. Как только вы вставляете изменяемый объект в Set / Map, если поле изменяется, то его хэш изменится, но его корзина — нет, что приводит к сбоям. Таким образом, варианты заключаются в том, чтобы не переопределять equals / GetHash для изменяемых объектов или придерживаться политики никогда не изменять изменяемый объект, как только он был помещен в коллекцию.

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

[2] Одним из примеров в нашей кодовой базе является графовая структура, где каждому узлу необходим уникальный идентификатор, который можно использовать для связывания узлов XML при окончательной записи в XML. Когда мы клонируем эти объекты, мы хотим, чтобы идентификатор был другим, но все остальное оставалось прежним. После более подробных размышлений об этом кажется, что вопросы «этот объект уже есть в этой коллекции» и «определены ли эти объекты одинаково», используют принципиально разные концепции равенства в этом контексте. Первый спрашивает об идентичности, и я бы хотел, чтобы идентификатор включался при глубоком сравнении, в то время как второй спрашивает о сходстве, и я не хочу, чтобы идентификатор включался. Это заставляет меня больше склоняться к реализации метода equals.

Ребята, вы согласны с этим решением или считаете, что реализация equals — лучший способ?

Комментарии:

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

2. Просто в сторону, вы можете захотеть взглянуть на org.apache.commons.lang.builder. EqualsBuilder#reflectionEquals, это звучит очень похоже на то, что вы реализовали с помощью своего метода contentEquals.

3. предложение @artbristol является хорошим, и с помощью EqualsBuilder вы можете указать, какие поля следует пропускать в методе equals. Я реализовал это в нашем проекте, и он уловил несколько моментов. Следует отметить одну вещь. В быстром тесте производительности, который я написал для сравнения реализаций equals, reflectionEquals занял в 4 раза больше времени, чем pojomatic, и в 25 раз больше, чем метод equals, созданный eclipse. Вы также можете использовать EqualsBuilder.append, который был создан между pojomatic и eclipse.

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

5. Согласен со всеми, что правильное использование equals() — плохая новость. Неизбежно будет код (например, как вы указываете, в коллекциях), который ожидает, что ваш equals () будет вести себя в соответствии с фактической семантикой неизменяемости и взаимозаменяемости между равными объектами, в то время как ваши объекты не являются неизменяемыми, а их экземпляры ‘equal’ не взаимозаменяемы.

Ответ №1:

Я бы выбрал подход reflection и определил пользовательскую аннотацию с помощью RetentionPolicy.СРЕДА ВЫПОЛНЕНИЯ, позволяющая разработчикам тестируемых классов отмечать поля, которые, как ожидается, изменятся после клонирования. Затем вы можете проверить аннотацию с помощью отражения и пропустить отмеченные поля.

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

Аннотация может выглядеть следующим образом:

 import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ChangesOnClone
{
}
  

Вот как это можно использовать в тестируемом коде:

 class ABC
{
     private String name;

     @ChangesOnClone
     private Cache cache;
}
  

И, наконец, соответствующая часть тестового кода:

 for ( Field field : fields )
{
    if( field.getAnnotation( ChangesOnClone.class ) )
        continue;
    // else test it
}
  

Комментарии:

1. Если вы собираетесь использовать reflection, я бы определенно НЕ стал внедрять вашу собственную версию, поскольку уже существуют работоспособные, протестированные реализации, такие как EqualsBuilder и Pojomatic.

2. Использование аннотаций — действительно хорошая идея. Это также позволит мне обрабатывать случаи, когда «равенство» клонирования и «равенство» сериализации различны.

3. Да, 1 за подход с аннотациями.

Ответ №2:

AssertJ предлагает рекурсивную функцию сравнения:

 assertThat(new).usingRecursiveComparison().isEqualTo(old);
  

Подробности смотрите в документации AssertJ: https://assertj.github.io/doc/#basic-usage

Предварительные условия для использования AssertJ:

импорт:

 import static org.assertj.core.api.Assertions.*;
  

зависимость maven:

     <!-- test -->
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.19.0</version>
        <scope>test</scope>
    </dependency>