Проверка на глубокое равенство в тестах 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
}