Тестирование на требуемое поведение против TDD - PullRequest
5 голосов
/ 15 сентября 2009

В статье Тест на требуемое поведение, а не случайное поведение , Кевлин Хенни советует нам, что:

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

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

А как насчет разделения их на отдельный набор тестов? Это звучит как начало, но интуитивно кажется непрактичным. Кто-нибудь делает это?

Ответы [ 4 ]

3 голосов
/ 17 сентября 2009

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

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

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

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

1 голос
/ 17 сентября 2009

«А как насчет разделения их на отдельный набор тестов?»

Что бы вы сделали с этим отдельным набором?

Вот типичный пример использования.

  1. Вы написали несколько тестов, которые проверяют детали реализации, которые они не должны были проверять.

  2. Вы выделяете эти тесты из основного набора в отдельный набор.

  3. Кто-то меняет реализацию.

  4. Ваш комплект реализации теперь не работает (как и должно быть).

Что теперь?

  • Исправить тесты реализации? Думаю, нет. Смысл состоял в том, чтобы не проверять реализацию, потому что это приводит к значительным затратам на техническое обслуживание.

  • Есть тесты, которые могут провалиться, но общий прогон юнит-теста все еще считается хорошим? Если тесты не пройдены, но неудача не имеет значения, что это вообще значит? [Прочитайте этот вопрос для примера: Некритические сбои тестирования юнитов Игнорируемое или нерелевантное испытание просто дорого.

Вы должны отказаться от них.

Сэкономьте себе время и обострения, отбросив их сейчас, а не тогда, когда они потерпят неудачу.

1 голос
/ 15 сентября 2009

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

Принцип DRY применяется как к тестовому коду, так и к рабочему коду. Это часто может быть хорошим ориентиром при написании тестового кода. Цель должна состоять в том, чтобы все «случайные» действия, которые вы указываете на этом пути, были изолированными, чтобы их использовали только несколько тестов из всего набора тестов. Таким образом, если вам нужно реорганизовать это поведение, вам нужно изменить только несколько тестов вместо большой части всего набора тестов.

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

Вот пример того, что я имею в виду. Предположим, что у вас есть какая-то реализация MVC, где Контроллер должен возвращать View. Предположим, что у нас есть такой метод на BookController:

public View DisplayBookDetails(int bookId)

Реализация должна использовать вставленный IBookRepository, чтобы получить книгу из базы данных, а затем преобразовать ее в представление этой книги. Вы можете написать множество тестов, чтобы охватить все аспекты метода DisplayBookDetails, но вы также можете сделать что-то еще:

Определите дополнительный интерфейс IBookMapper и добавьте его в BookController в дополнение к IBookRepository. Реализация метода может быть примерно такой:

public View DisplayBookDetails(int bookId)
{
    return this.mapper.Map(this.repository.GetBook(bookId);
}

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

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

0 голосов
/ 17 сентября 2009

Я на самом деле у TDD проблема не так велика, как может показаться сразу, потому что вы пишете тесты до кода. Вы не должны даже думать о какой-либо возможной реализации перед написанием теста.

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

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

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

  • наиболее распространенным таким тестом, вероятно, является случай, когда результаты тестирования происходят в каком-то определенном порядке, игнорируя этот порядок, действительно не гарантируется. Легкое исправление достаточно просто: сортируйте как результат, так и ожидаемый результат. Для более сложных структур используйте некоторый компаратор, который игнорирует такие различия.

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

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

  • еще один плохой случай - когда мы написали тесты, используя какой-то Mocked-объект (используемый в качестве заглушки), а затем изменили поведение mocked-объекта (API Change). Это плохо, потому что он не нарушает код, когда должен, потому что изменение поведения макета объекта не изменит Mock, имитирующего его. Исправление здесь состоит в том, чтобы использовать реальный объект вместо макета, если это возможно, или исправить макет для нового поведения. В этом случае как поведение Mock, так и поведение реального объекта являются случайными, но мы считаем, что тесты, которые не дают ошибок, когда они должны быть, представляют собой большую проблему, чем тесты, ломающиеся, когда они не должны. (Надо полагать, что такие случаи также могут решаться на уровне интеграционных тестов).

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