В другом вопросе было выявлено, что одной из проблем TDD является синхронизация набора тестов с базой кода во время и после рефакторинга.
Теперь я большой поклонник рефакторинга. Я не собираюсь бросать это, чтобы сделать TDD. Но я также сталкивался с проблемами тестов, написанных таким образом, что незначительный рефакторинг приводит к большому количеству неудачных тестов.
Как избежать взлома тестов при рефакторинге?
- Вы пишете тесты «лучше»? Если так, что вы должны искать?
- Вы избегаете определенных видов рефакторинга?
- Существуют ли инструменты тестирования-рефакторинга?
Изменить: я написал новый вопрос, который спросил, что я хотел спросить (но сохранил этот как интересный вариант).
development-process
tdd
refactoring
Алекс Фейнман
источник
источник
Ответы:
То, что вы пытаетесь сделать, это на самом деле не рефакторинг. С помощью рефакторинга, по определению, вы не меняете то, что делает ваше программное обеспечение, вы меняете то, как оно делает это.
Начните со всех зеленых тестов (все проходят), затем внесите изменения «под капот» (например, переместите метод из производного класса в базовый, извлеките метод или инкапсулируйте Composite с помощью Builder и т. Д.). Ваши тесты все еще должны пройти.
То, что вы описываете, похоже, не рефакторинг, а редизайн, который также увеличивает функциональность тестируемого программного обеспечения. TDD и рефакторинг (как я здесь определил) не конфликтуют. Вы все еще можете выполнить рефакторинг (зелено-зеленый) и применить TDD (красно-зеленый), чтобы развить функциональность «дельта».
источник
Одним из преимуществ модульных тестов является возможность уверенного рефакторинга.
Если рефакторинг не меняет общедоступный интерфейс, то вы оставляете модульные тесты как есть и после рефакторинга убедитесь, что все они прошли.
Если рефакторинг действительно изменяет открытый интерфейс, то тесты должны быть сначала переписаны. Рефакторинг, пока не пройдут новые испытания.
Я бы никогда не избежал рефакторинга, потому что он ломает тесты. Написание юнит-тестов может быть болью в заднице, но это стоит боли в долгосрочной перспективе.
источник
Вопреки другим ответам, важно отметить, что некоторые способы тестирования могут стать хрупкими при реорганизации тестируемой системы (SUT), если тест представляет собой whitebox.
Если я использую фреймворк, который проверяет порядок методов, вызываемых на макетах (когда порядок не имеет значения, поскольку вызовы не имеют побочных эффектов); тогда, если мой код будет чище с этими вызовами методов в другом порядке, и я проведу рефакторинг, мой тест будет прерван. В общем, макеты могут вносить хрупкость в тесты.
Если я проверяю внутреннее состояние моего SUT, выставляя его закрытых или защищенных членов (мы можем использовать «друг» в Visual Basic или повысить уровень доступа «внутренний» и использовать «internalsvisibleto» в c #; во многих языках ОО, включая c # можно использовать « подкласс специфичного для теста »), тогда внезапно будет иметь значение внутреннее состояние класса - возможно, вы реорганизуете класс как черный ящик, но тесты белого ящика не пройдут. Предположим, что одно поле повторно используется для обозначения разных вещей (не очень хорошая практика!), Когда SUT изменяет состояние - если мы разделим его на два поля, нам может потребоваться переписать неработающие тесты.
Специфичные для теста подклассы также могут использоваться для тестирования защищенных методов - это может означать, что рефакторинг с точки зрения производственного кода является принципиальным изменением с точки зрения тестового кода. Перемещение нескольких строк в защищенный метод или из него может не иметь побочных эффектов производства, но может нарушить тест.
Если я использую « тестовые зацепки » или любой другой специфичный для теста или условный код компиляции, может быть трудно гарантировать, что тесты не сломаются из-за хрупких зависимостей от внутренней логики.
Таким образом, чтобы предотвратить привязку тестов к внутренним деталям SUT, это может помочь:
Все вышеперечисленные пункты являются примерами соединения белого ящика, используемого в тестах. Поэтому, чтобы полностью избежать рефакторинга тестов на взлом, используйте тестирование SUT в «черном ящике».
Отказ от ответственности: Чтобы обсудить рефакторинг здесь, я использую слово более широко, чтобы включить изменение внутренней реализации без каких-либо видимых внешних эффектов. Некоторые пуристы могут не соглашаться и ссылаться исключительно на книгу Мартина Фаулера и Кента Бека «Рефакторинг», в которой описываются операции атомного рефакторинга.
На практике мы склонны предпринимать несколько большие неразрывные шаги, чем описанные там атомарные операции, и, в частности, изменения, из-за которых производственный код ведет себя идентично извне, могут не оставить прохождение тестов. Но я думаю, что было бы справедливо включить «алгоритм замены для другого алгоритма, который имеет идентичное поведение» в качестве рефакторинга, и я думаю, что Фаулер с этим согласен. Сам Мартин Фаулер говорит, что рефакторинг может нарушить тесты:
источник
Если ваши тесты прерываются, когда вы выполняете рефакторинг, то вы, по определению, не выполняете рефакторинг, который «меняет структуру вашей программы без изменения поведения вашей программы».
Иногда вам действительно нужно изменить поведение ваших тестов. Возможно, вам нужно объединить два метода (скажем, bind () и listen () в классе прослушивающего сокета TCP), чтобы другие части вашего кода пытались и не смогли использовать теперь измененный API. Но это не рефакторинг!
источник
Я думаю, что проблема этого вопроса в том, что разные люди по-разному воспринимают слово «рефакторинг». Я думаю, что лучше тщательно определить несколько вещей, которые вы, вероятно, имеете в виду:
Как уже заметил другой человек, если вы сохраняете API-интерфейс одинаковым и все ваши регрессионные тесты работают с открытым API-интерфейсом, у вас не должно возникнуть проблем. Рефакторинг не должен вызывать никаких проблем. Любые неудачные тесты ДАЛИ означают, что в вашем старом коде была ошибка, а ваш тест плохой, или в вашем новом коде есть ошибка.
Но это довольно очевидно. Таким образом, вы, вероятно, подразумеваете под рефакторингом, что вы меняете API.
Итак, позвольте мне ответить, как подойти к этому!
Сначала создайте NEW API, который делает то, что вы хотите, чтобы ваше поведение NEW API было. Если случится так, что этот новый API будет иметь то же имя, что и OLDER API, тогда я добавлю имя _NEW к новому имени API.
int DoSomethingInterestingAPI ();
будет выглядеть так:
ОК - на данном этапе - все ваши регрессионные тесты пройдут - используя имя DoSomethingInterestingAPI ().
ДАЛЕЕ, просмотрите код и измените все вызовы DoSomethingInterestingAPI () на соответствующий вариант DoSomethingInterestingAPI_NEW (). Это включает в себя обновление / переписывание любых частей ваших регрессионных тестов, которые необходимо изменить для использования нового API.
СЛЕДУЮЩИЙ, отметьте DoSomethingInterestingAPI_OLD () как [[deprecated ()]]. Держите устаревший API столько, сколько хотите (пока вы не обновите весь код, который может от него зависеть).
При таком подходе любые сбои в ваших регрессионных тестах просто являются ошибками в этом регрессионном тесте или идентифицируют ошибки в вашем коде - именно так, как вы хотели бы. Этот поэтапный процесс пересмотра API путем явного создания версий API _NEW и _OLD позволяет вам иметь биты нового и старого кода, сосуществующие некоторое время.
источник
Я предполагаю, что ваши модульные тесты имеют гранулярность, которую я бы назвал «глупой» :), т.е. они проверяют абсолютные детали каждого класса и функции. Отойдите от инструментов генератора кода и напишите тесты, которые применимы к большей поверхности, тогда вы можете реорганизовать внутренние компоненты столько, сколько захотите, зная, что интерфейсы к вашим приложениям не изменились, и ваши тесты по-прежнему работают.
Если вы хотите иметь модульные тесты, которые тестируют каждый метод, то ожидайте, что придется проводить их рефакторинг одновременно.
источник
Что делает это сложным, так это сцепление . Любые тесты имеют некоторую степень связи с деталями реализации, но модульные тесты (независимо от того, является ли это TDD или нет) особенно плохи в этом, потому что они мешают внутренним компонентам: больше модульных тестов равняется большему количеству кода, связанного с модулями, то есть сигнатурами методов / любым другим открытым интерфейсом единиц - по крайней мере.
«Единицы» по определению являются деталями реализации низкого уровня, интерфейс модулей может и должен изменяться / разделяться / объединяться и иным образом изменяться по мере развития системы. Обилие юнит-тестов может на самом деле препятствовать этой эволюции больше, чем помогает.
Как избежать взлома тестов при рефакторинге? Избегайте сцепления. На практике это означает, что следует избегать как можно большего количества модульных тестов и предпочитать тесты более высокого уровня / интеграции, более независимые от деталей реализации. Помните, однако, что серебряной маркировки нет, тесты все равно должны соединяться с чем-то на каком-то уровне, но в идеале это должен быть интерфейс, который явно версии с использованием семантического контроля версий, то есть обычно на уровне опубликованного API / приложения (вы не хотите делать SemVer за каждую единицу в вашем решении).
источник
Ваши тесты слишком тесно связаны с реализацией, а не с требованием.
попробуйте написать свои тесты с комментариями, подобными этим:
таким образом, вы не можете изменить смысл тестов.
источник