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

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

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

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

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

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

Любые комментарии? Как вы относились к этому вопросу в прошлом?

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

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

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

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

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

Ответы

Ответ 1

Я бы пошел с подходом к отражению и определил пользовательскую аннотацию с RetentionPolicy.RUNTIME, чтобы позволить разработчикам тестируемых классов отмечать поля, которые, как ожидается, будут изменены после клонирования. Затем вы можете проверить аннотацию с отражением и пропустить отмеченные поля.

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

Аннотация могла бы выглядеть так:

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
}