Имеет ли смысл писать тесты для унаследованного кода, когда нет времени для полного рефакторинга?


72

Я обычно стараюсь следовать советам книги Эффективная работа с унаследованным Cod е . Я нарушаю зависимости, перемещаю части кода в @VisibleForTesting public staticметоды и в новые классы, чтобы сделать код (или, по крайней мере, его часть) тестируемым. И я пишу тесты, чтобы убедиться, что я ничего не нарушаю, когда я изменяю или добавляю новые функции.

Коллега говорит, что я не должен этого делать. Его рассуждения:

  • Первоначально исходный код может не работать должным образом. А написание тестов для него затрудняет будущие исправления и модификации, поскольку разработчики должны понимать и модифицировать тесты.
  • Если это GUI-код с некоторой логикой (~ 12 строк, 2-3 блока if / else, например), тест не стоит проблем, так как код слишком тривиален для начала.
  • Подобные плохие шаблоны могут существовать и в других частях кодовой базы (чего я еще не видел, я довольно новый); будет проще очистить их все в одном большом рефакторинге. Извлечение логики может подорвать эту возможность в будущем.

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


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

3
на то, что следует классифицировать как ошибку, могут полагаться другие части кода, превращающие его в функцию
трещотка, урод

1
Единственный хороший аргумент против того, о чем я могу подумать, это то, что сам ваш рефакторинг может привести к новым ошибкам, если вы что-то неправильно прочитали / скопировали. По этой причине я могу реорганизовать и исправить все, что душе угодно, в версии, находящейся в стадии разработки, но любые исправления в предыдущих версиях сталкиваются с гораздо более серьезными препятствиями и могут не быть одобрены, если они являются «только» косметической / структурной очисткой, поскольку считается, что риск превышает потенциальную выгоду. Знайте свою местную культуру - не только представление одного коровника - и имейте ЧРЕЗВЫЧАЙНО веские основания, прежде чем делать что-либо еще.
Кешлам

6
Первый пункт довольно забавный - «Не проверяйте это, возможно, он содержит ошибки». Ну, да? Тогда хорошо это знать - либо мы хотим это исправить, либо мы не хотим, чтобы кто-то изменил реальное поведение на то, что было сказано в какой-то спецификации проекта. В любом случае, тестирование (и запуск тестов в автоматизированной системе) выгодно.
Кристофер Кройциг

3
Слишком часто «один большой рефакторинг», который должен произойти и который излечит все беды, - это миф, придуманный теми, кто просто хочет отодвинуть вещи, которые они считают скучными (написание тестов), в далекое будущее. И если это когда-нибудь станет реальностью, они будут серьезно сожалеть о том, что позволили ему стать таким большим!
Джулия Хейворд

Ответы:


100

Вот мое личное ненаучное впечатление: все три причины звучат как широко распространенные, но ложные когнитивные иллюзии.

  1. Конечно, существующий код может быть неправильным. Это также может быть правильно. Поскольку приложение в целом имеет ценность для вас (в противном случае вы просто от него отказались бы), при отсутствии более конкретной информации следует исходить из того, что оно в основном верно. «Написание тестов усложняет ситуацию, потому что в целом задействовано больше кода» - это упрощенное и очень неправильное отношение.
  2. Во что бы то ни стало затрачивайте усилия по рефакторингу, тестированию и улучшению в тех местах, где они приносят наибольшую пользу с наименьшими усилиями. Подпрограммы GUI форматирования значений часто не являются первоочередными задачами. Но не проверять что-то, потому что «это просто» - тоже очень неправильное отношение. Практически все серьезные ошибки совершаются, потому что люди думали, что они поняли что-то лучше, чем они на самом деле.
  3. «Мы сделаем все это одним махом в будущем» - это хорошая мысль. Обычно большой удар остается твердым в будущем, в то время как в настоящем ничего не происходит. Я твердо убежден в том, что «медленно и уверенно выигрывает гонку».

23
+1 за «Практически все серьезные ошибки совершаются, потому что люди думали, что понимают что-то лучше, чем они на самом деле».
Рем

По пункту 1 - с BDD тесты самодокументируются ...
Робби Ди

2
Как указывает @ guillaume31, часть ценности написания тестов демонстрирует, как на самом деле работает код - что может соответствовать или не соответствовать спецификации. Но это может быть «неправильной» спецификацией: бизнес-потребности могли измениться, и код отражает новые требования, а спецификация - нет. Просто предполагать, что код «неправильный», слишком упрощенно (см. Пункт 1). И снова тесты скажут вам, что на самом деле делает код, а не то, что кто-то думает / говорит, что делает (см. Пункт 2).
Дэвид

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

50

Несколько мыслей:

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

Я бы согласился с тем, что чистый код GUI сложно тестировать и, возможно, он плохо подходит для рефакторинга в стиле «Работаем эффективно ... ». Однако это не означает, что вы не должны извлекать поведение, которое не имеет ничего общего с уровнем графического интерфейса пользователя, и тестировать извлеченный код. И «12 строк, 2-3 блока if / else» не тривиальны. Весь код, имеющий хотя бы немного условной логики, должен быть протестирован.

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

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


5
+1 за «тесты, которые ты пишешь, проверь текущее поведение программы »
Дэвид

17

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


К сожалению, правда. У нас есть пара очевидных ошибок, которые проявляются в крайних случаях, которые мы не можем исправить, потому что наш клиент предпочитает последовательность вместо правильности. (Они вызваны тем, что код сбора данных допускает вещи, которые код отчетности не учитывает, например, оставляя одно поле в серии полей пустым)
Izkata

14

Ответ Килиана охватывает наиболее важные аспекты, но я хочу остановиться на пунктах 1 и 3.

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

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

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

Также есть некоторые соображения по поводу пункта 3: в дополнение к тому факту, что «большой мах» редко случается, есть еще одна вещь: на самом деле это не так просто. Чтобы было проще, необходимо выполнить несколько условий:

  • Антипаттерн, подлежащий рефакторингу, должен быть легко найден. Названы ли все ваши синглтоны XYZSingleton? Всегда ли вызывается их экземпляр getter getInstance()? И как вы находите свои слишком глубокие иерархии? Как вы ищете ваши объекты бога? Для этого требуется анализ метрик кода, а затем проверка метрик вручную. Или ты просто спотыкаешься о них, когда работаешь, как делал.
  • Рефакторинг должен быть механическим. В большинстве случаев трудной частью рефакторинга является понимание существующего кода достаточно хорошо, чтобы знать, как его изменить. Опять синглтоны: если синглтон пропал, как вы получаете необходимую информацию для его пользователей? Часто это означает понимание местного коллграфа, чтобы вы знали, откуда взять информацию. Теперь, что проще: найти десять синглетов в вашем приложении, понять их использование (что приводит к необходимости понимать 60% кодовой базы) и извлечь их? Или взять код, который вы уже понимаете (потому что вы работаете над ним прямо сейчас), и разорвать синглтоны, которые там используются? Если рефакторинг не настолько механичен, что практически не требует знания окружающего кода, его объединять бесполезно.
  • Рефакторинг должен быть автоматизирован. Это несколько основано на мнении, но здесь идет. Немного рефакторинга - это весело и сытно. Много рефакторинга утомительно и скучно. Оставив кусок кода, над которым вы только что поработали, в лучшем состоянии, вы получите приятное, теплое чувство, прежде чем переходить к более интересным вещам. Попытка рефакторинга всей кодовой базы оставит вас разочарованными и злыми на идиотских программистов, которые написали это. Если вы хотите провести рефакторинг большого свупа, то он должен быть в значительной степени автоматизирован, чтобы минимизировать разочарование. В каком-то смысле это сочетание первых двух моментов: вы можете автоматизировать рефакторинг только в том случае, если вы можете автоматизировать поиск плохого кода (то есть легко найти) и автоматизировать его изменение (т.е. механическое).
  • Постепенное улучшение способствует лучшему экономическому обоснованию. Рефакторинг с большим махом невероятно разрушителен. Если вы реорганизуете фрагмент кода, вы неизбежно вступаете в конфликты слияния с другими людьми, работающими над ним, потому что вы просто разделяете метод, который они меняли, на пять частей. Когда вы реорганизуете кусок кода разумного размера, вы получаете конфликты с несколькими людьми (1-2 при разделении мегафункции на 600 строк, 2-4 при разрушении объекта бога, 5 при извлечении синглтона из модуля ), но у вас все равно были бы эти конфликты из-за ваших основных правок. Когда вы делаете рефакторинг на базе кода, вы конфликтуете со всеми, Не говоря уже о том, что он связывает несколько разработчиков на несколько дней. Постепенное улучшение заставляет каждую модификацию кода занимать немного больше времени. Это делает его более предсказуемым, и нет такого видимого периода времени, когда ничего не происходит, кроме очистки.

12

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

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

Я лично подписан на Принцип Бойскаута, но другие (как вы видели) этого не делают.

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

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

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


2
Я полностью согласен в принципе, но во многих компаниях это сводится к времени и деньгам. Если «приведение в порядок» занимает всего несколько минут, то это нормально, но как только оценка для приведения в порядок начинает увеличиваться (для некоторого определения большого), вам, специалисту по кодированию, необходимо делегировать это решение вашему боссу или менеджер проектов. Это не ваше место, чтобы решить, сколько времени вы потратили. Работа над исправлением ошибки X или новой функцией Y может иметь гораздо большую ценность для проекта / компании / клиента.
Оз

2
Вы также можете не знать о более крупных проблемах, таких как прекращение проекта через 6 месяцев или просто то, что компания ценит ваше время больше (например, вы делаете что-то, что они считают более важным, а кто-то другой выполняет работу по рефакторингу). Рефакторинг работы также может повлиять на тестирование. Будет ли большой рефакторинг вызвать полную регрессию теста? Есть ли у компании ресурсы, которые она может использовать для этого?
Оз

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

4

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

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

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


3

Re: «Исходный код может не работать должным образом»:

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


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

3

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

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

Если я правильно помню, книга «Рефакторинг» в любом случае говорит, что нужно постоянно тестировать, так что это часть процесса.


3

У автоматизированного тестирования покрытия.

Остерегайтесь желаемых мыслей, как своих, так и ваших клиентов и начальников. Как бы мне ни хотелось верить, что мои изменения будут правильными в первый раз, и мне придется проверять только один раз, я научился относиться к такого рода мышлению так же, как к нигерийским мошенническим письмам. Ну, в основном; Я никогда не обращался к мошенническим письмам, но недавно (когда орал) я отказался от использования лучших практик. Это был болезненный опыт, который тянул (дорого) снова и снова. Больше никогда!

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

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


1

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

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

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

IntegrationTestCase1()
{
    var input = ReadDataFile("path\to\test\data\case1in.ext");
    bool validInput = ValidateData(input);
    Assert.IsTrue(validInput);

    var processedData = ProcessData(input);
    Assert.AreEqual(0, processedData.Errors.Count);

    bool writeError = WriteFile(processedData, "temp\file.ext");
    Assert.IsFalse(writeError);

    bool filesAreEqual = CompareFiles("temp\file.ext", "path\to\test\data\case1out.ext");
    Assert.IsTrue(filesAreEqual);
}

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

Поскольку вы можете расширить охват модульных тестов, вы можете сократить или даже отказаться от большинства интеграционных тестов. Если ваше приложение читает / записывает файлы или обращается к БД, тестирование этих частей по отдельности и либо их макетирование, либо начало ваших тестов с создания структур данных, считанных из файла / базы данных, являются очевидным местом для начала. На самом деле создание этой инфраструктуры тестирования займет намного больше времени, чем написание набора быстрых и грязных тестов; и каждый раз, когда вы запускаете 2-минутный набор интеграционных тестов, вместо того, чтобы тратить 30 минут на ручное тестирование части того, что охватывало интеграционные тесты, вы уже добиваетесь больших успехов.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.