Зачем писать тесты для кода, который я буду рефакторинг?

15

Я рефакторинг огромного унаследованного кода класса. Рефакторинг (я полагаю) защищает это:

  1. написать тесты для унаследованного класса
  2. рефакторинг, черт возьми, из класса

Проблема: после того, как я проведу рефакторинг класса, мои тесты на шаге 1 нужно будет изменить. Например, то, что раньше было в унаследованном методе, теперь может быть отдельным классом. То, что было одним методом, теперь может быть несколькими методами. Весь ландшафт унаследованного класса может быть стерт с лица земли во что-то новое, и поэтому тесты, которые я напишу на шаге 1, будут практически недействительными. По сути, я буду добавлять Шаг 3. переписать мои тесты обильно

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

Это реальная причина писать тесты перед рефакторингом - чтобы помочь мне лучше понять код? Должна быть другая причина!

Пожалуйста, объясни!

Замечания:

Есть такая запись: есть ли смысл писать тесты для унаследованного кода, когда нет времени для полного рефакторинга? но там написано «написать тесты перед рефакторингом», но не сказано «почему» или что делать, если «написание тестов» кажется «занятой работой, которая скоро будет уничтожена»

Деннис
источник
1
Ваша предпосылка неверна. Вы не будете менять свои тесты. Вы будете писать новые тесты. Шаг 3 будет «удалить все тесты, которые больше не существуют».
pdr
1
Шаг 3 может затем прочитать «Написать новые тесты. Удалить несуществующие тесты». Я думаю, что это все равно сводится к уничтожению оригинальной работы
Деннис
3
Нет, вы хотите написать новые тесты на шаге 2. И да, шаг 1 уничтожен. Но была ли это пустая трата времени? Нет, потому что это дает вам массу заверений в том, что вы ничего не сломаете на шаге 2. Ваши новые тесты не делают.
pdr
3
@Dennis - хотя я разделяю многие ваши опасения в отношении ситуаций, мы могли бы рассматривать большинство усилий по рефакторингу как «уничтожение оригинальной работы», но если бы мы никогда не уничтожили ее, мы бы никогда не отошли от спагетти-кода с 10 тыс. Строк в одной файл. То же самое, вероятно, должно пойти на модульные тесты, они идут рука об руку с кодом, который они тестируют. По мере развития кода и перемещения и / или удаления вещей, должны развиваться и модульные тесты.
ДХМ
«Понимание кода» - немалое преимущество. Как вы ожидаете рефакторинга программы, которую вы не понимаете? Это неизбежно, и какой лучший способ продемонстрировать истинное понимание программы, чем написать тщательный тест. Также следует сказать, что чем более абстрактно тестирование, тем меньше вероятность того, что вам придется его потом поцарапать, так что, во всяком случае, сначала остановитесь на высокоуровневом тестировании.
Нил

Ответы:

46

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

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

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

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

Амон
источник
6
+1. Прочитал мои мысли, написал мой ответ. Важный момент: вам может понадобиться написать модульные тесты, чтобы показать, что те же самые ошибки остаются после рефакторинга!
david.pfx
Вопрос: почему в вашем примере изменения имени функции вы сначала меняете тест, чтобы убедиться, что он не пройден? Я хочу сказать, что, конечно, произойдет сбой, когда вы измените его - вы разорвали соединение, используемое компоновщиками, чтобы связать код вместе! Возможно, вы ожидаете, что может существовать другая существующая частная функция с именем, которое вы только что выбрали, и вы должны убедиться, что это не тот случай, если вы ее пропустили? Я вижу, что это даст вам определенную гарантию, граничащую с ОКР, но в этом случае это похоже на избыточное убийство. Есть ли когда-нибудь возможная причина, по которой тест в вашем примере не провалится?
Денис
^ cont: в качестве общего метода я вижу, что полезно сделать пошаговую проверку работоспособности вашего кода, чтобы как можно раньше выявить, что что-то идет не так. Вроде как вы можете не заболеть, если не будете мыть руки каждый раз, но простое мытье рук, как привычка, сохранит ваше здоровье в целом, независимо от того, вступаете ли вы в контакт с загрязненными предметами или нет. Здесь вы можете время от времени мыть руки излишне или время от времени излишне тестировать код, но это помогает поддерживать здоровье вас и вашего кода. Это то, что вы имели в виду?
Денис
@ На самом деле, Денис, я неосознанно описывал научно правильный эксперимент: мы не можем сказать, какой параметр на самом деле повлиял на результат, если больше изменить один параметр. Помните, что тесты - это код, и каждый код содержит ошибки. Вы попадете в ад программистов за то, что не запускали тесты, прежде чем касаться кода? Конечно, нет: во время проведения тестов было бы идеально, это ваше профессиональное решение, если это необходимо. Далее обратите внимание, что тест не прошел, если он не компилируется, и что мой ответ также применим к динамическим языкам, а не только к статическим языкам с компоновщиком.
Амон
2
От исправления различных ошибок во время рефакторинга я понимаю, что я бы не делал код так легко, не имея тестов. Тесты предупреждают меня о поведенческих / функциональных «различиях», которые я представляю, изменяя свой код.
Деннис
7

Ах, поддержание устаревших систем.

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

Редактировать: скажем, часть вашего кода измеряет что-то, и у него есть функция, которая просто возвращает значение. Единственный интерфейс - это вызов функции / метода / whatnot и получение возвращаемого значения. Это слабое соединение и его легко тестировать. Если ваша основная программа имеет подкомпонент, который управляет буфером, и все обращения к нему зависят от самого буфера, некоторых управляющих переменных, и она возвращает сообщения об ошибках через другой раздел кода, то вы можете сказать, что он тесно связан, и это трудно для модульного теста. Вы все еще можете сделать это с достаточным количеством поддельных объектов и еще много чего, но это становится грязным. Особенно в ц. Любое количество рефакторинга, как работает буфер, сломает подкомпонент.
Конец Править

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

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

Хотя вы можете разделить метод на три, они все равно будут делать то же, что и предыдущий метод, поэтому вы можете пройти тест для старого метода и разделить его на три. Усилия по написанию первого теста не пропали даром.

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

Филипп
источник
Я думаю, что понимаю, но вы потеряли меня на интерфейсах. т.е. тесты, которые я пишу сейчас, проверяют, правильно ли заполнены определенные переменные, после вызова тестируемого метода. Если эти переменные будут изменены или подвергнутся рефакторингу, то будут и мои тесты. Существующий унаследованный класс, с которым я работаю, не имеет интерфейсов / геттеров / сеттеров, которые могли бы вносить переменные изменения или делать их менее трудоемкими. Но опять же, я не уверен, что вы подразумеваете под интерфейсами, когда речь идет об устаревшем коде. Может быть, я могу создать некоторые? Но это будет рефакторинг.
Денис
1
Да, если у вас есть один божественный класс, который делает все, тогда действительно нет никаких интерфейсов. Но если он вызывает другой класс, высший класс ожидает, что он будет вести себя определенным образом, и модульные тесты могут это проверить. Тем не менее, я бы не стал делать вид, что вам не придется обновлять тестирование вашего модуля во время рефакторинга.
Филипп
4

Мой собственный ответ / реализация:

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

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

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

ОБНОВИТЬ

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

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

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

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

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

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

Майкл Даррант
источник
Ваш первый абзац, кажется, выступает за игнорирование шага 1 и написание тестов по ходу дела; ваш второй абзац, кажется, противоречит этому.
pdr
Обновил мой ответ.
Майкл Даррант
2

Какова цель рефакторинга в вашем конкретном случае?

Предположим, что в моем согласии с моим ответом мы все (до некоторой степени) верим в TDD (разработка через тестирование).

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

  • Тесты помогут вам убедиться, что ваша новая работа действительно работает.

  • Тесты, вероятно, также выявят случаи, когда оригинальная работа не работает.

Но как вы на самом деле делаете какой-либо значительный рефакторинг, не влияя до некоторой степени на поведение ?

Вот краткий список нескольких вещей, которые могут произойти во время рефакторинга:

  • переименовать переменную
  • переименовать функцию
  • добавить функцию
  • удалить функцию
  • разделить функцию на две или более функции
  • объединить две или более функций в одну функцию
  • сплит класс
  • объединить занятия
  • переименовать класс

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

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

Может быть, поведение не меняется на макроуровне, но цель модульного тестирования не в том, чтобы гарантировать поведение макроса. Это интеграционное тестирование. Цель модульного тестирования - убедиться, что отдельные кусочки, из которых вы строите свой продукт, не сломаны. Цепочка, самое слабое звено и т. Д.

Как насчет этого сценария:

  • Предположим, у вас есть function bar()

  • function foo() звонит bar()

  • function flee() также делает вызов функции bar()

  • Просто для разнообразия, flam()звонитfoo()

  • Все работает великолепно (по-видимому, по крайней мере).

  • Вы рефакторинг ...

  • bar() переименован в barista()

  • flee() изменено на вызов barista()

  • foo()это не изменено на вызовbarista()

Очевидно, ваши тесты для обоих foo()и flam()сейчас провалились.

Может быть, вы не поняли, foo()позвонил bar()в первую очередь. Вы, конечно, не понимали, что flam()зависит bar()от способа foo().

Без разницы. Суть в том, что ваши тесты обнаружат недавно нарушенное поведение обоих foo()и flam(), в пошаговом порядке, во время вашей работы по рефакторингу.

Тесты в итоге помогут вам хорошо провести рефакторинг.

Если у вас нет никаких тестов.

Это немного надуманный пример. Есть те, кто будет утверждать, что если бы смена bar()перерывов foo(), то foo()была слишком сложной для начала и должна быть разбита. Но процедуры могут вызывать другие процедуры по причине, и невозможно устранить всю сложность, верно? Наша задача - справляться со сложностью достаточно хорошо.

Рассмотрим другой сценарий.

Вы строите здание.

Вы строите леса, чтобы гарантировать, что здание построено правильно.

Строительные леса, среди прочего, помогут вам построить шахту лифта. После этого вы сносите леса, но шахта лифта остается. Вы уничтожили «оригинальную работу», уничтожив леса.

Аналогия незначительна, но суть в том, что не случайно создать инструменты, которые помогут вам создать продукт. Даже если инструменты не являются постоянными, они полезны (даже необходимы). Плотники постоянно делают джиг, иногда только на одну работу. Затем они разрывают пазухи, иногда используя детали, чтобы построить другие пазухи для других работ, иногда нет. Но это не делает джиг бесполезным или напрасным усилием.

Craig
источник