Проверка на глубокое равенство в тестах JUnit - PullRequest
14 голосов
/ 20 мая 2011

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

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

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

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

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

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

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

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

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

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

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

1 Ответ

5 голосов
/ 20 мая 2011

Я бы пошел с подходом отражения и определил пользовательскую аннотацию с помощью 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
}
...