Должен ли я юнит-тестировать мои подклассы или мой абстрактный родительский класс?

12

У меня есть скелетная реализация, как в пункте 18 из Effective Java (расширенное обсуждение здесь ). Это абстрактный класс, который предоставляет 2 открытых метода methodA () и methodB (), которые вызывают методы подклассов для «заполнения пробелов», которые я не могу определить абстрактно.

Сначала я разработал его, создав конкретный класс и написав для него модульные тесты. Когда пришел второй класс, я смог извлечь общее поведение, реализовать «пропущенные пробелы» во втором классе, и он был готов (конечно, для второго подкласса были созданы модульные тесты).

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

Моя проблема в том, что когда я создаю новую реализацию, я пишу тесты снова и снова:

  • Вызывает ли подкласс данный обязательный метод?
  • Имеет ли данный метод данную аннотацию?
  • Получу ли я ожидаемые результаты от метода А?
  • Получу ли я ожидаемые результаты от метода B?

Пока вижу преимущества с таким подходом:

  • Это документирует требования подкласса через тесты
  • Может быстро потерпеть неудачу в случае проблемного рефакторинга
  • Протестируйте подкласс в целом и через его открытый API («работает ли methodA ()?», А не «работает ли этот защищенный метод?»)

Проблема, с которой я столкнулся, заключается в том, что тесты для нового подкласса, в основном, просты: - тесты одинаковы во всех подклассах, они просто меняют тип возвращаемого значения методов (реализация скелета является общей) и свойства, которые я проверьте на утверждении часть теста. - Обычно подклассы не имеют специфичных для них тестов.

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

Распространена ли эта проблема при тестировании иерархии классов? Как я могу избежать этого?

ps: я думал о тестировании скелетного класса, но также кажется странным создание макета абстрактного класса, чтобы иметь возможность его протестировать. И я думаю, что любой рефакторинг абстрактного класса, который изменяет поведение, которое не ожидается подклассом, не будет замечен так быстро. Мои смелости говорят мне, что тестирование подкласса "в целом" является предпочтительным подходом здесь, но, пожалуйста, не стесняйтесь сказать мне, что я ошибаюсь :)

PS2: я погуглил и нашел много вопросов по этому вопросу. Одним из них является тот, который имеет отличный ответ от Найджела Торна, мой случай был бы его "номером 1". Это было бы здорово, но я не могу рефакторинг на этом этапе, поэтому мне придется смириться с этой проблемой. Если бы я мог провести рефакторинг, у меня был бы каждый подкласс в качестве стратегии. Это было бы хорошо, но я все равно протестировал бы «основной класс» и протестировал бы стратегии, но я не заметил бы никакого рефакторинга, который нарушил бы интеграцию между основным классом и стратегиями.

PS3: я нашел несколько ответов о том, что «приемлемо» тестировать абстрактный класс. Я согласен, что это приемлемо, но я хотел бы знать, какой подход является предпочтительным в этом случае (я все еще начинаю с юнит-тестов)

И.С.Бах
источник
2
Напишите абстрактный тестовый класс и сделайте так, чтобы ваши конкретные тесты наследовали его, отражая структуру бизнес-кода. Тесты должны проверять поведение, а не реализацию модуля, но это не значит, что вы не можете использовать свои внутренние знания о системе для более эффективного достижения этой цели.
Килиан Фот
Я где-то читал, что нельзя использовать наследование в тестах, я ищу источник, чтобы прочитать его внимательно
JSBach
4
@KilianFoth вы уверены в этом совете? Это звучит как рецепт для хрупких модульных тестов, очень чувствительных к изменениям конструкции и, следовательно, не очень ремонтопригодных.
Конрад Моравский

Ответы:

5

Я не понимаю, как бы вы написали тесты для абстрактного базового класса; Вы не можете создать его экземпляр, поэтому у вас не может быть экземпляра для тестирования. Вы могли бы создать «фиктивный», конкретный подкласс только для целей его тестирования - не уверен, насколько это будет полезно. YMMV.

Каждый раз, когда вы создаете новую реализацию (класс), вы должны писать тесты, которые осуществляют эту новую реализацию. Так как части функциональности (ваши «пробелы») реализованы в каждом подклассе, это абсолютно необходимо; выходные данные каждого из унаследованных методов могут (и, вероятно, должны ) быть различными для каждой новой реализации.

Фил В.
источник
Да, они разные (типы и содержание разные). Технически говоря, я мог бы смоделировать этот класс или иметь простую реализацию только для целей тестирования с пустыми реализациями. Мне не нравится такой подход. Но тот факт, что мне «не нужно» думать о том, чтобы сдавать тесты в новых реализациях, заставляет меня чувствовать себя странно, а также заставляет меня задаться вопросом о том, насколько я уверен в этом тесте (конечно, если я изменю базовый класс специально, тесты не пройдены, что является доказательством того, что я могу им доверять)
JSBach
4

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

Преимущества этого метода в том, что, поскольку вы протестировали интерфейс, теперь вы можете выполнить рефакторинг под интерфейсом. Вы можете обнаружить, что код в дочернем классе должен быть в родительском, или наоборот. Теперь ваши тесты докажут, что вы не нарушали поведение при перемещении этого кода.

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

Майкл К
источник