Хорошо протестированная кодовая база имеет ряд преимуществ, но при тестировании определенных аспектов системы получается кодовая база, устойчивая к некоторым типам изменений.
Примером является тестирование на конкретный вывод - например, текст или HTML. Тесты часто (наивно?) Написаны так, чтобы ожидать, что определенный блок текста будет выводиться для некоторых входных параметров, или для поиска определенных разделов в блоке.
Изменение поведения кода для соответствия новым требованиям или из-за того, что юзабилити-тестирование привело к изменению интерфейса, также требует изменения тестов - возможно, даже тестов, которые не являются конкретно юнит-тестами для изменяемого кода.
Как вы управляете работой по поиску и переписыванию этих тестов? Что, если вы не можете просто «запустить их всех и позволить фреймворку разобраться с ними»?
Какие еще виды тестируемого кода приводят к обычно хрупким тестам?
источник
Ответы:
Я знаю, что люди из TDD будут ненавидеть этот ответ, но большая часть его для меня - тщательно выбирать, где что-то тестировать.
Если я схожу с ума от юнит-тестов на нижних уровнях, то никакие значимые изменения не могут быть сделаны без изменения юнит-тестов. Если интерфейс никогда не раскрывается и не предназначен для повторного использования вне приложения, тогда это просто ненужные накладные расходы на то, что в противном случае могло бы быть быстрым изменением.
И наоборот, если то, что вы пытаетесь изменить, раскрывается или повторно используется, то каждый из этих тестов, которые вам придется изменить, является свидетельством того, что вы можете нарушить в другом месте.
В некоторых проектах это может означать разработку ваших тестов от уровня приемки вниз, а не от модульных тестов вверх. и иметь меньше юнит-тестов и больше тестов стиля интеграции.
Это не означает, что вы по-прежнему не можете идентифицировать одну функцию и код, пока эта функция не соответствует критериям приемлемости. Это просто означает, что в некоторых случаях вы не в конечном итоге измеряете критерии приемки с помощью модульных тестов.
источник
Я только что завершил капитальный ремонт своего стека SIP, переписав весь транспорт TCP. (Это был почти рефакторинг, в довольно большом масштабе, по сравнению с большинством рефакторингов.)
Вкратце, есть TIdSipTcpTransport, подкласс TIdSipTransport. Все TIdSipTransports имеют общий набор тестов. Внутри TIdSipTcpTransport было несколько классов - карта, содержащая пары соединения / инициирующего сообщения, многопоточные TCP-клиенты, многопоточный TCP-сервер и так далее.
Вот что я сделал:
Таким образом, я знал, что мне еще нужно было сделать, в виде закомментированных тестов (*), и знал, что новый код работает должным образом, благодаря новым тестам, которые я написал.
(*) Действительно, вам не нужно их комментировать. Просто не запускайте их; 100 провальных тестов не очень обнадеживают. Кроме того, в моем конкретном случае компиляция меньшего количества тестов означает более быстрый цикл test-write-refactor.
источник
Когда тесты хрупкие, я нахожу их обычно, потому что я тестирую не то, что нужно. Взять, к примеру, вывод HTML. Если вы проверите фактический вывод HTML, ваш тест будет хрупким. Но вас не интересует фактический результат, вас интересует, передает ли он информацию, которая должна. К сожалению, для этого необходимо сделать утверждения о содержимом мозгов пользователя и поэтому не может быть сделано автоматически.
Вы можете:
То же самое происходит с SQL. Если вы утверждаете фактический SQL, ваши классы попытаются заставить вас столкнуться с проблемами. Вы действительно хотите утверждать результаты. Поэтому я использую базу данных памяти SQLITE во время своих модульных тестов, чтобы убедиться, что мой SQL действительно выполняет то, что должен.
источник
Сначала создайте 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).
источник