Как вы эффективно поддерживаете свои тесты, работая над редизайном?

14

Хорошо протестированная кодовая база имеет ряд преимуществ, но при тестировании определенных аспектов системы получается кодовая база, устойчивая к некоторым типам изменений.

Примером является тестирование на конкретный вывод - например, текст или HTML. Тесты часто (наивно?) Написаны так, чтобы ожидать, что определенный блок текста будет выводиться для некоторых входных параметров, или для поиска определенных разделов в блоке.

Изменение поведения кода для соответствия новым требованиям или из-за того, что юзабилити-тестирование привело к изменению интерфейса, также требует изменения тестов - возможно, даже тестов, которые не являются конкретно юнит-тестами для изменяемого кода.

  • Как вы управляете работой по поиску и переписыванию этих тестов? Что, если вы не можете просто «запустить их всех и позволить фреймворку разобраться с ними»?

  • Какие еще виды тестируемого кода приводят к обычно хрупким тестам?

Алекс Фейнман
источник
Чем это существенно отличается от programmers.stackexchange.com/questions/5898/… ?
AShelly
4
Этот вопрос был ошибочно задан о рефакторинге - модульные тесты должны быть инвариантными при рефакторинге.
Алекс Фейнман

Ответы:

9

Я знаю, что люди из TDD будут ненавидеть этот ответ, но большая часть его для меня - тщательно выбирать, где что-то тестировать.

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

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

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

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

Билл
источник
Я думаю, что вы хотели написать «вне модуля», а не «вне приложения».
SamB
СамБ, это зависит. Если интерфейс является внутренним для нескольких мест в рамках одного приложения, но не общедоступным, я бы рассмотрел возможность тестирования на более высоком уровне, если бы думал, что интерфейс, вероятно, будет нестабильным.
Билл
Я обнаружил, что этот подход очень совместим с TDD. Мне нравится начинать с верхних уровней приложения ближе к конечному пользователю, чтобы я мог проектировать нижние уровни, зная, как верхние уровни должны использовать нижние уровни. По сути, построение сверху вниз позволяет более точно спроектировать интерфейс между одним слоем и другим.
Грег Бургхардт
4

Я только что завершил капитальный ремонт своего стека SIP, переписав весь транспорт TCP. (Это был почти рефакторинг, в довольно большом масштабе, по сравнению с большинством рефакторингов.)

Вкратце, есть TIdSipTcpTransport, подкласс TIdSipTransport. Все TIdSipTransports имеют общий набор тестов. Внутри TIdSipTcpTransport было несколько классов - карта, содержащая пары соединения / инициирующего сообщения, многопоточные TCP-клиенты, многопоточный TCP-сервер и так далее.

Вот что я сделал:

  • Удалил классы, которые я собирался заменить.
  • Удалены тестовые наборы для этих классов.
  • Оставил набор тестов, специфичный для TIdSipTcpTransport (и все еще был набор тестов, общий для всех TIdSipTransports).
  • Запустите тесты TIdSipTransport / TIdSipTcpTransport, чтобы убедиться, что все они не пройдены.
  • Прокомментированы все тесты TIdSipTransport / TIdSipTcpTransport, кроме одного.
  • Если бы мне нужно было добавить класс, я бы добавил его для написания тестов, чтобы создать достаточно функциональности, чтобы пройти единственный комментарий без комментариев.
  • Вспенить, промыть, повторить.

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

(*) Действительно, вам не нужно их комментировать. Просто не запускайте их; 100 провальных тестов не очень обнадеживают. Кроме того, в моем конкретном случае компиляция меньшего количества тестов означает более быстрый цикл test-write-refactor.

Фрэнк Шиарар
источник
Я тоже делал это несколько месяцев назад, и это сработало для меня. Однако я не мог абсолютно применить этот метод при объединении с коллегой при первоначальной переработке нашего модуля модели предметной области (что, в свою очередь, привело к изменению дизайна всех других модулей в проекте).
Марко Чамброне
3

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

Вы можете:

  • Сгенерируйте HTML как дымовой тест, чтобы убедиться, что он действительно работает
  • Используйте систему шаблонов, чтобы вы могли протестировать процессор шаблонов и данные, отправленные в шаблон, без фактического тестирования самого шаблона.

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

Уинстон Эверт
источник
Это также может помочь в использовании структурного HTML.
SamB
@SamB, конечно, это помогло бы, но я не думаю, что это решит проблему полностью
Уинстон Эверт
конечно нет, ничего не могу :-)
SamB
-1

Сначала создайте NEW API, который делает то, что вы хотите, чтобы ваше поведение NEW API было. Если случится так, что этот новый API будет иметь то же имя, что и OLDER API, тогда я добавлю имя _NEW к новому имени API.

int DoSomethingInterestingAPI ();

будет выглядеть так:

int DoSomethingInterestingAPI_NEW (int take_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (what_default_mimics_the_old_API); Хорошо - на этом этапе - все ваши регрессионные тесты пройдут - используя имя DoSomethingInterestingAPI ().

ДАЛЕЕ, просмотрите код и измените все вызовы DoSomethingInterestingAPI () на соответствующий вариант DoSomethingInterestingAPI_NEW (). Это включает в себя обновление / переписывание любых частей ваших регрессионных тестов, которые необходимо изменить, чтобы использовать новый API.

ДАЛЕЕ отметьте DoSomethingInterestingAPI_OLD () как [[deprecated ()]]. Держите устаревший API столько, сколько хотите (пока вы не обновите весь код, который может от него зависеть).

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

Вот хороший (сложный) пример такого подхода на практике. У меня была функция BitSubstring () - где я использовал подход, при котором третий параметр был COUNT битов в подстроке. Чтобы соответствовать другим API и шаблонам в C ++, я хотел переключиться на начало / конец в качестве аргументов функции.

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

Я создал функцию BitSubstring_NEW с новым API и обновил весь мой код, чтобы использовать его (оставив NO MORE CALLS для BitSubString). Но я оставил в реализации несколько выпусков (месяцев) - и отметил, что это устарело - чтобы каждый мог переключиться на BitSubString_NEW (и в это время изменить аргумент со счетчика на стиль начала / конца).

Затем - когда этот переход был завершен, я сделал еще один коммит, удалив BitSubString () и переименовав BitSubString_NEW-> BitSubString () (и объявил устаревшим имя BitSubString_NEW).

Льюис Прингл
источник
Никогда не добавляйте суффиксы, которые не имеют никакого смысла или самоуничижительны для имен. Всегда старайтесь давать значимые имена.
Basilevs
Вы полностью упустили суть. Во-первых, это не суффиксы, которые «не имеют смысла». Они несут в себе смысл того, что API переходит от старого к более новому. На самом деле, в этом весь смысл ВОПРОСА, на который я отвечал, и весь смысл ответа. Имена CLEARLY сообщают, что является OLD API, который является NEW API, и который в конечном итоге является целевым именем API после завершения перехода. И - суффиксы _OLD / _NEW являются временными - ТОЛЬКО при переходе изменения API.
Льюис Прингл
Удачи в NEW_NEW_3 версии API три года спустя.
Васильев