Я прочитал некоторые ответы на вопросы в том же ключе, например, «Как вы поддерживаете свои юнит-тесты при рефакторинге?». В моем случае сценарий немного отличается тем, что мне дали проект для проверки и приведения в соответствие с некоторыми нашими стандартами, в настоящее время тестов для проекта вообще нет!
Я определил ряд вещей, которые, я думаю, можно было бы сделать лучше, например, НЕ смешивать код типа DAO на уровне сервиса.
Перед рефакторингом было неплохо написать тесты для существующего кода. Мне кажется, что проблема заключается в том, что когда я выполняю рефакторинг, эти тесты ломаются, поскольку я меняю место выполнения определенной логики, и тесты будут записываться с учетом предыдущей структуры (ложные зависимости и т. Д.)
В моем случае, что было бы лучшим способом продолжить? У меня возникает соблазн написать тесты для реорганизованного кода, но я знаю, что есть риск, что я могу неправильно реорганизовать вещи, которые могут изменить желаемое поведение.
Является ли это рефакторингом или редизайном, я рад, что мое понимание этих терминов подлежит исправлению, в настоящее время я работаю над следующим определением рефакторинга: «Рефакторинг по определению не меняет то, что делает ваше программное обеспечение, вы меняете то, как он это делает. Таким образом, я не изменяю то, что делает программное обеспечение, я изменяю как / где это делает.
В равной степени я вижу аргумент, что если я изменяю сигнатуру методов, это можно считать редизайном.
Вот краткий пример
MyDocumentService.java
(ток)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
MyDocumentService.java
(переделано / переработано что угодно)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
//Code dealing with DataResultSet moved back up to DAO
//DAO now returns a List<Document> instead of a DataResultSet
return documentDAO.findAllDocuments();
}
}
источник
Ответы:
Вы ищете тесты, которые проверяют регрессии . то есть нарушая какое-то существующее поведение. Я бы начал с определения того, на каком уровне это поведение останется прежним, и что интерфейс, управляющий этим поведением, останется прежним, и начну тестирование с этого момента.
Теперь у вас есть несколько тестов, которые подтвердят, что независимо от того, что вы делаете ниже этого уровня, ваше поведение остается прежним.
Вы совершенно правы, спрашивая, как тесты и код могут оставаться в синхронизации. Если ваш интерфейс к компоненту остается прежним, вы можете написать тест для этого и установить одинаковые условия для обеих реализаций (при создании новой реализации). Если это не так, то вы должны признать, что тест для избыточного компонента является избыточным тестом.
источник
Рекомендуемая практика - начинать с написания «тестов с фиксацией», которые проверяют текущее поведение кода, возможно, включая ошибки, но без необходимости погружаться в безумие, позволяющее определить, является ли данное поведение, нарушающее документы требований, ошибкой, обходной путь для чего-то, о чем вы не знаете или представляете недокументированное изменение требований.
Наиболее целесообразно, чтобы эти тесты с закреплением находились на высоком уровне, то есть интеграции, а не модульных тестах, чтобы они продолжали работать, когда вы начнете рефакторинг.
Но некоторые рефакторинги могут быть необходимы, чтобы сделать код тестируемым - просто будьте осторожны и придерживайтесь «безопасных» рефакторингов. Например, почти во всех случаях частные методы могут быть обнародованы, ничего не нарушая.
источник
Я предлагаю - если вы еще этого не сделали - прочитать как Эффективную работу с унаследованным кодом, так и рефакторинг - Улучшение дизайна существующего кода .
Я не обязательно видеть это как проблему: Создать тесты, изменить структуру кода, а затем настроить тестовую структуру также . Это даст вам прямую обратную связь о том, действительно ли ваша новая структура на самом деле лучше старой, потому что если это так, то скорректированные тесты будет легче писать (и, следовательно, изменение тестов должно быть относительно простым, что снижает риск появления новой системы). ошибка пройти тесты).
Кроме того, как уже писали другие: не пишите слишком подробные тесты (по крайней мере, не в начале). Старайтесь оставаться на высоком уровне абстракции (таким образом, ваши тесты, вероятно, лучше охарактеризовать как регрессионные или даже интеграционные тесты).
источник
Не пишите строгие модульные тесты, где вы высмеиваете все зависимости. Некоторые люди скажут вам, что это не настоящие юнит-тесты. Игнорируй их. Эти тесты полезны, и вот что важно.
Давайте посмотрим на ваш пример:
Ваш тест, вероятно, выглядит примерно так:
Вместо того, чтобы издеваться над DocumentDao, смоделируйте его зависимости:
Теперь вы можете переместить логику из
MyDocumentService
вDocumentDao
без испытаний ломающихся. Тесты покажут, что функциональность одинакова (насколько вы ее тестировали).источник
Как вы говорите, если вы измените поведение, то это трансформация, а не рефакторинг. На каком уровне вы меняете поведение - вот что имеет значение.
Если формальных тестов на самом высоком уровне не существует, попробуйте найти набор требований, которые клиенты (вызывающий код или люди) должны оставить прежними после изменения дизайна, чтобы ваш код считался работающим. Вот список тестовых примеров, которые вам нужно реализовать.
Чтобы ответить на ваш вопрос об изменении реализаций, требующих изменения тестовых случаев, я бы посоветовал вам взглянуть на TDD Детройт (классический) против Лондон (mockist) Мартин Фаулер рассказывает об этом в своей замечательной статье. « Насмешки - это не тупики, но у многих людей есть мнения. Если вы начнете с самого высокого уровня, где ваши внешние возможности не могут измениться, и продолжите свой путь вниз, тогда требования должны оставаться достаточно стабильными, пока вы не достигнете уровня, который действительно должен измениться.
Без каких-либо тестов это будет сложно, и вы можете рассмотреть возможность запуска клиентов по двойным путям кода (и записи различий), пока вы не будете уверены, что ваш новый код делает именно то, что ему нужно.
источник
Здесь мой подход. Это имеет стоимость с точки зрения времени, потому что это рефактор-тест в 4 этапа.
То, что я собираюсь показать, может лучше подойти в компонентах с большей сложностью, чем в примере, приведенном в вопросе.
В любом случае стратегия действительна для любого кандидата компонента, который должен быть нормализован интерфейсом (DAO, Services, Controllers, ...).
1. Интерфейс
Давайте соберем все открытые методы из MyDocumentService и позволим объединить их все в интерфейс. Например. Если он уже существует, используйте его вместо установки нового .
Затем мы заставляем MyDocumentService реализовать этот новый интерфейс.
Все идет нормально. Никаких серьезных изменений сделано не было, мы соблюдали действующий договор, и поведение остается неизменным.
2. Модульное тестирование устаревшего кода
Здесь у нас тяжелая работа. Чтобы настроить тестовый набор. Мы должны установить как можно больше случаев: успешные случаи, а также ошибки. Это последнее во благо качества результата.
Теперь вместо тестирования MyDocumentService мы будем использовать интерфейс в качестве проверяемого контракта.
Я не буду вдаваться в подробности, так что прости меня, если мой код выглядит слишком простым или слишком агностичным
Этот этап занимает больше времени, чем любой другой в этом подходе. И это самое главное, потому что он будет служить ориентиром для будущих сравнений.
Примечание: из-за каких-либо серьезных изменений не было сделано, и поведение остается нетронутым. Я предлагаю сделать тег здесь в SCM. Тег или ветка не имеют значения. Просто сделай версию.
Мы хотим сделать это для отката, сравнения версий и, возможно, для параллельного выполнения старого кода и нового.
3. Рефакторинг
Рефакторинг будет внедрен в новый компонент. Мы не будем вносить никаких изменений в существующий код. Первый шаг так же прост, как скопировать и вставить MyDocumentService и переименовать его в CustomDocumentService (например).
Новый класс продолжает реализовывать DocumentService . Затем перейдите и выполните рефакторизацию getAllDocuments () . (Давайте начнем с одного. Pin-рефакторы)
Это может потребовать некоторых изменений в интерфейсе / методах DAO. Если это так, не меняйте существующий код. Реализуйте свой собственный метод в интерфейсе DAO. Аннотируйте старый код как устаревший, и позже вы узнаете, что следует удалить.
Важно не нарушать / не изменять существующую реализацию. Мы хотим выполнить оба сервиса параллельно, а затем сравнить результаты.
4. Обновление DocumentServiceTestSuite
Хорошо, теперь самая легкая часть. Добавить тесты нового компонента.
Теперь у нас есть oldResult и newResult, которые проверены независимо, но мы также можем сравнивать друг с другом. Эта последняя проверка является необязательной и зависит от результата. Может быть, это не сравнимо.
Может не иметь большого смысла сравнивать две коллекции таким образом, но будет допустимо для любого другого типа объекта (pojos, сущности модели данных, DTO, Wrappers, нативные типы ...)
Примечания
Я не осмелился бы рассказать, как проводить юнит-тесты или как использовать фиктивные библиотеки. Я не смею ни говорить, как вы должны сделать рефакторинг. Я хотел предложить глобальную стратегию. Как это сделать, зависит от вас. Вы точно знаете, каков код, его сложность и стоит ли попробовать такую стратегию. Здесь важны такие факты, как время и ресурсы. Также имеет значение то, что вы ожидаете от этих тестов в будущем.
Я начал свои примеры с Службы, и я следовал бы с DAO и так далее. Углубляясь в уровни зависимости. Более или менее это может быть описано как восходящая стратегия. Тем не менее, для незначительных изменений / рефакторинга ( как тот, что показан в примере с туром ), восходящая задача облегчит задачу. Потому что масштаб изменений невелик.
Наконец, вы должны удалить устаревший код и перенаправить старые зависимости на новый.
Удалите также устаревшие тесты и работа сделана. Если вы версировали старое решение с его тестами, вы можете проверять и сравнивать друг друга в любое время.
В результате такого большого количества работы у вас есть унаследованный код, проверенный и проверенный. И новый код, проверенный, проверенный и готовый к версии.
источник
tl; dr Не пишите юнит-тесты. Пишите тесты на более подходящем уровне.
Учитывая ваше рабочее определение рефакторинга:
там очень широкий спектр. С одной стороны - автономное изменение конкретного метода, возможно, с использованием более эффективного алгоритма. На другом конце портируется на другой язык.
Независимо от уровня рефакторинга / редизайна, важно иметь тесты, которые работают на этом уровне или выше.
Автоматизированные тесты часто классифицируются по уровню как:
Модульные тесты - отдельные компоненты (классы, методы)
Интеграционные тесты - взаимодействие между компонентами
Системные тесты - полное приложение
Напишите уровень теста, который может выдержать рефакторинг практически без изменений.
Думать:
источник
Не тратьте время на написание тестов, которые подключаются в тех местах, где вы можете ожидать, что интерфейс изменится нетривиальным образом. Это часто является признаком того, что вы пытаетесь провести юнит-тестирование классов, которые являются «совместными» по своей природе - ценность которых не в том, что они делают сами, а в том, как они взаимодействуют с рядом тесно связанных классов, чтобы произвести ценное поведение , Это то поведение, которое вы хотите протестировать, что означает, что вы хотите тестировать на более высоком уровне. Тестирование ниже этого уровня часто требует большого количества уродливых насмешек, и результирующие тесты могут быть скорее тормозом развития, чем защитой поведения.
Не зацикливайтесь на том, делаете ли вы рефакторинг, редизайн или что-то еще. Вы можете внести изменения, которые на более низком уровне представляют собой перепроектирование ряда компонентов, но на более высоком уровне интеграции просто равняются рефакторингу. Суть в том, чтобы понять, какое поведение имеет ценность для вас, и защищать это поведение на ходу.
При написании тестов может быть полезно подумать - могу ли я легко описать QA, владельцу продукта или пользователю, что этот тест на самом деле тестирует? Если кажется, что описание теста будет слишком эзотерическим и техническим, возможно, вы тестируете не на том уровне. Проводите тестирование на тех уровнях / уровнях, которые «имеют смысл», и не путайте свой код с тестами на каждом уровне.
источник
Ваша первая задача - попытаться придумать «идеальную сигнатуру метода» для ваших тестов. Стремитесь сделать это чистой функцией . Это должно быть независимо от кода, который на самом деле тестируется; это небольшой слой адаптера. Напишите свой код для этого слоя адаптера. Теперь, когда вы реорганизуете свой код, вам нужно только изменить уровень адаптера. Вот простой пример:
Тесты хороши, но у тестируемого кода плохой API. Я могу изменить его, не меняя тесты, просто обновив уровень моего адаптера:
Этот пример кажется довольно очевидным в соответствии с принципом «Не повторяй себя», но в других случаях он может быть не столь очевиден. Преимущество выходит за пределы DRY - реальным преимуществом является отделение тестов от тестируемого кода.
Конечно, этот метод не может быть целесообразным во всех ситуациях. Например, не было бы никакой причины писать адаптеры для POCO / POJO, потому что у них действительно нет API, который мог бы изменяться независимо от тестового кода. Также, если вы пишете небольшое количество тестов, сравнительно большой уровень адаптера, вероятно, будет напрасной тратой усилий.
источник