#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>