Чи є сенс писати тести на застарілий код, коли немає часу на повне рефакторинг?


72

Я, як правило, намагаюся дотримуватися порад книги, що працює ефективно з Legacy Cod e . Я розбиваю залежності, переміщую частини коду до @VisibleForTesting public staticметодів і до нових класів, щоб зробити код (або хоча б якусь його частину) перевіряемою. І я пишу тести, щоб переконатися, що нічого не зламаю, коли змінюю або додаю нові функції.

Колега каже, що я не повинен цього робити. Його міркування:

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

Чи слід уникати витягування деталей, що перевіряються, та написання тестів, якщо ми не маємо часу для повного рефакторингу? Чи є якийсь недолік у цьому, який я повинен врахувати?


29
Здається, ваш колега просто пред'являє виправдання, оскільки він не працює так. Люди іноді так поводяться через те, що вони занадто наполегливі, щоб змінити свій прийнятий спосіб робити.
Док Браун

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

1
Єдиний хороший аргумент, про який я можу подумати, - це те, що ваш рефакторинг сам може ввести нові помилки, якщо ви щось неправильно прочитали / неправильно скопіювали. З цієї причини я вільний переробити і виправити вміст мого серця на версії, що наразі розробляється, але будь-які виправлення попередніх версій стикаються з набагато більшими труднощами і не можуть бути затверджені, якщо вони "лише" косметичні / структурні очищення, оскільки вважається, що ризик перевищує потенційний прибуток. Знайте свою місцеву культуру - не лише одну ідею корова-копача - і майте НАДАГО важкі причини, перш ніж робити щось інше.
keshlam

6
Перший момент є веселим - "Не тестуйте, це може бути помилкою". Ну, так? Тоді добре це знати - або ми хочемо це виправити, або ми не хочемо, щоб хтось міняв фактичну поведінку на те, про що говорили деякі технічні характеристики. У будь-якому випадку тестування (і запуск тестів в автоматизованій системі) вигідне.
Крістофер Кройцгіг

3
Занадто часто "один великий рефакторинг", який має відбутися, і який вилікує всі недуги, - це міф, сфабрикований тими, хто просто хоче просунути речі, які вони вважають нудними (писати тести) у далеке майбутнє. І якщо це коли-небудь стане реальним, вони серйозно пошкодують, що дозволили йому стати настільки великим!
Джулія Хейвард

Відповіді:


100

Ось моє особисте ненаукове враження: всі три причини звучать як поширені, але помилкові пізнавальні ілюзії.

  1. Звичайно, існуючий код може бути неправильним. Це також може бути правильним. Оскільки програма в цілому, здається, має для вас цінність (інакше ви її просто відкинете), за відсутності більш конкретної інформації слід вважати, що вона є переважно правильною. "Написання тестів робить речі складнішими, оскільки в цілому більше задіяного коду" - це спрощене і дуже неправильне ставлення.
  2. Одним чином витрачайте свої рефакторинг, тестування та вдосконалення в місцях, де вони додають найбільшу цінність з найменшими зусиллями. Підпрограми графічного графічного форматування значення часто не є першим пріоритетом. Але не тестувати щось, тому що "це просто" - це також дуже неправильне ставлення. Практично всі серйозні помилки допущені тому, що люди думали, що вони розуміють щось краще, ніж насправді.
  3. "Ми зробимо це все одним махом у майбутньому" - це приємна думка. Зазвичай великий настрій міцно тримається в майбутньому, тоді як в сьогоденні нічого не відбувається. Мені, я твердо переконаний у тому, що "повільний і стійкий виграє гонку".

23
+1 за "Практично всі серйозні помилки допущені, тому що люди думали, що вони розуміють щось краще, ніж насправді".
бер

Повторний пункт 1 - з BDD тести є самодокументуванням ...
Роббі Ді

2
Як зазначає @ guillaume31, частина значення написання тестів демонструє, як насправді працює код - який може бути, а може і не відповідати специфікаціям. Але це може бути специфікація, яка "неправильна": потреби бізнесу можуть змінитися, і код відображає нові вимоги, але специфікація цього не робить. Просте припущення, що код "неправильний", є надто спрощеним (див. Пункт 1). І знову тести покажуть вам, що насправді робить код, а не те, що хтось думає / каже, що робить (див. Пункт 2).
Девід

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

50

Кілька думок:

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

Я погоджуюся, що чистий код GUI важко перевірити і, можливо, не підходить для рефакторингу стилю " Ефективна робота ... " Однак це не означає, що не слід вилучати поведінку, яка не має нічого спільного в шарі GUI, і перевіряти витягнутий код. І "12 рядків, 2-3 if / else блок" не є тривіальним. Весь код з принаймні трохи умовної логіки повинен бути перевірений.

На мій досвід, великі реконструкції непрості, і вони рідко працюють. Якщо ви не ставите перед собою точних, крихітних цілей, існує високий ризик, що ви приступите до нескінченного переробки волосся, де врешті-решт ніколи не приземлитеся на ноги. Чим більша зміна, тим більше ви ризикуєте щось зламати і тим більше проблем у вас з’явиться, де ви провалилися.

Покращення прогресивної роботи за допомогою спеціальних невеликих реконструкцій не "підриває майбутні можливості", це дозволяє їм - твердіти болотисту землю, де лежить ваша програма. Ви обов'язково повинні це зробити.


5
+1 за "тести, які ви пишете, перевіряйте поточну поведінку програми "
Девід

17

Також повторіть: "Оригінальний код може не працювати належним чином" - це не означає, що ви просто змінюєте поведінку коду, не переживаючи про вплив. Інший код може покладатися на те, що видається порушеним поведінкою або побічними ефектами поточної реалізації. Тестове покриття існуючої програми повинно полегшити переробку пізніше, оскільки це допоможе вам дізнатися, коли ви випадково щось зламали. Спершу слід перевірити найважливіші частини.


На жаль правда. У нас є декілька очевидних помилок, які виявляються у кращих випадках, які ми не можемо виправити, оскільки наш клієнт віддає перевагу послідовності над правильністю. (Вони спричинені завдяки коду збору даних, що дозволяє коду звіту не враховувати, як, наприклад, залишити одне поле в ряді полів порожнім)
Izkata

14

Відповідь Кіліана охоплює найважливіші аспекти, але я хочу розкрити питання 1 та 3.

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

Якщо є тести, то вона також повинна зрозуміти тести. У той же час тести повинні допомогти їй зрозуміти основний код, і тести зрозуміти набагато простіше, ніж функціональний код все одно (якщо тільки вони погані тести). І тести допомагають показати, що змінилося в поведінці старого коду. Навіть якщо оригінальний код неправильний, а тестові тести на неправильну поведінку, це все-таки є перевагою.

Однак для цього потрібно, щоб тести були задокументовані як тестування попередньої поведінки, а не специфікації.

Деякі думки щодо пункту 3 теж: окрім того, що "великий наліт" рідко буває насправді, є й інша річ: насправді це не простіше. Щоб було легше, потрібно було б застосувати кілька умов:

  • Антипаттерн, який підлягає реконструкції, потребує легкого пошуку. Чи названі всі ваші одиночні XYZSingleton? Чи завжди називаються їхні екземпляри getInstance()? І як ти знаходиш свої надто глибокі ієрархії? Як ви шукаєте предмети свого бога? Для них потрібен аналіз кодових метрик, а потім ручний огляд показників. Або ти просто натикаєшся на них, як працюєш, як робив.
  • Рефакторинг повинен бути механічним. У більшості випадків важка частина рефакторингу достатньо добре розуміє існуючий код, щоб знати, як його змінити. Знову одинарні: якщо сингл пішов, як ви отримуєте необхідну інформацію для його користувачів? Це часто означає розуміння місцевого телефонного опису, щоб ви знали, звідки брати інформацію. Тепер що простіше: пошук десяти синглонів у вашій програмі, розуміння використання кожного (що призводить до необхідності розуміння 60% кодової бази) та видобування їх? Або взяти код, який ви вже розумієте (адже ви над цим працюєте зараз) і видобути синглів, які там використовуються? Якщо рефакторинг не настільки механічний, що вимагає мало знань про оточуючий код, використання в його з’єднанні немає.
  • Рефакторинг потрібно автоматизувати. Це дещо ґрунтується на думці, але тут ідеться. Трохи рефакторингу - це весело і ситно. Багато рефакторингу стомлює і нудно. Залишаючи шматочок коду, над яким ви тільки працювали, в кращому стані дає гарне, тепле відчуття, перш ніж переходити до більш цікавих речей. Спроба змінити всю базу коду залишить вас розчарованим і злим на ідіотських програмістів, які це написали. Якщо ви хочете зробити великий рефакторинг, то його потрібно значною мірою автоматизувати, щоб мінімізувати розчарування. Це, певним чином, поле перших двох пунктів: ви можете автоматизувати рефакторинг лише тоді, коли зможете автоматизувати пошук поганого коду (тобто легко знайти) та автоматизувати його зміну (тобто механічну).
  • Поступове вдосконалення сприяє кращому бізнесу. Рефакторинг з великим набором неймовірно руйнує. Якщо ви переробляєте фрагмент коду, ви незмінно вступаєте в конфлікти злиття з іншими людьми, які працюють над ним, тому що ви просто розділили метод, який вони змінювали, на п’ять частин. Коли ви рефакторируете фрагмент коду з досить великим розміром, ви стикаєтеся з конфліктом з кількома людьми (1-2 при розбитті мегафункції 600 рядків, 2-4 при розбиванні об'єкта God, 5 під час вилучення сингтона з модуля ), але у вас все одно виникли б ці конфлікти через основні зміни. Коли ви робите рефакторинг на основі кодової бази, ви конфліктуєте з усіма. Не кажучи вже про те, що це пов'язує декількох розробників протягом днів. Поступове вдосконалення змушує кожну зміну коду тривати трохи більше часу. Це робить його більш передбачуваним, і немає такого видимого періоду часу, коли нічого не відбувається, крім очищення.

12

У деяких компаніях існує культура, де вони схильні, щоб дозволити розробникам в будь-який час вдосконалювати код, який безпосередньо не надає додаткової цінності, наприклад, нові функції.

Я, мабуть, проповідую перетвореному тут, але це явно помилкова економія. Чистий і стислий код приносить користь наступним розробникам. Просто окупність виявляється не відразу.

Я особисто підписуюся на принципі хлопців-скаутів, але інші (як ви бачили) цього не роблять.

Однак, програмне забезпечення страждає від ентропії та створює технічну заборгованість. Невдовзі попередні розробники (або, можливо, ліниві чи недосвідчені), можливо, впровадили неоптимальні глючні рішення над добре розробленими. Незважаючи на те, що може здатися бажаним переробити їх, ви ризикуєте ввести нові помилки до того, що є (для користувачів у будь-якому випадку) робочим кодом.

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

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


2
Я в принципі повністю згоден, але в багатьох компаніях це зводиться до часу і грошей. Якщо частина "охайного" займає лише кілька хвилин, то це нормально, але як тільки оцінка за охайність починає збільшуватися (для певного визначення великої), вам, кодуючий особі, потрібно делегувати це рішення своєму начальнику або керівник проекту. Ви не вирішуєте значення витраченого часу. Робота над виправленням помилок X або новою функцією Y може мати набагато більшу цінність для проекту / компанії / замовника.
ozz

2
Ви також можете не знати про більші проблеми, такі як проект, який забирають за 6 місяців, або просто те, що компанія цінує ваш час більше (наприклад, ви робите щось, що вважаєте важливішим, а хтось інший повинен виконувати роботу з реконструкції). Робота рефакторингу також може вплинути на тестування. Чи спровокує великий рефакторинг повний тест регресії? Чи є у компанії ресурси, які вона може розгорнути для цього?
ozz

Так, коли ви торкнулися, існує безліч причин, через які основна операція з кодом може бути або не є хорошою ідеєю: інші пріоритети розробки, термін експлуатації програмного забезпечення, ресурс тестування, досвід розробника, з'єднання, цикл випуску, знайомство з кодом база, документація, критичність місії, культура компанії тощо тощо. Це виклик рішення
Роббі Ді

4

На моєму досвіді добре працює тест на характеристику . Він дає вам досить широке, але не дуже конкретне покриття тесту порівняно швидко, але його можна складно реалізувати для додатків GUI.

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

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


3

Re: "Оригінальний код може не працювати належним чином":

Тести не пишуться каменем. Їх можна змінити. І якщо ви перевірили на предмет, який був неправильним, слід легко переписати тест правильніше. Зрештою, змінився лише очікуваний результат перевіреної функції.


1
ІМО, окремі тести повинні бути написані каменем, принаймні до того, як функція, яку вони тестують, загинула і пішла. Саме вони підтверджують поведінку існуючої системи та допомагають запевнити обслуговуючого персоналу, що їх зміни не порушать застарілого коду, який вже може покладатися на цю поведінку. Змініть тести на живу функцію, і ви видалите ці запевнення.
cHao

3

Ну так. Відповідає як інженер з тестування програмного забезпечення. По-перше, ви повинні протестувати все, що коли-небудь робити. Тому що якщо ви цього не зробите, ви не знаєте, працює він чи ні. Це може здатися нам очевидним, але у мене є колеги, які бачать це інакше. Навіть якщо ваш проект є маленьким, який ніколи не може бути доставлений, ви повинні подивитися користувача в обличчя і сказати, що ви знаєте, що він працює, тому що ви його протестували.

Нетривіальний код завжди містить помилки (цитування хлопця з uni; і якщо в ньому немає ніяких помилок, це тривіально), і наша робота полягає в тому, щоб знайти їх до того, як це зробить клієнт. У застарілому коді є застарілі помилки. Якщо оригінальний код працює не так, як слід, ви хочете знати про нього, повірте. З помилками все в порядку, якщо ви знаєте про них, не бійтеся їх знайти, ось для чого потрібні нотатки до випуску.

Якщо я правильно пам’ятаю, книга «Рефакторинг» говорить про те, що в будь-якому разі постійно тестуватись, тож це частина процесу.


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);
}

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

Оскільки ви зможете розширити покриття тестових одиниць, ви можете зменшити чи навіть звільнити більшість інтеграційних тестів. Якщо ваш додаток читає / записує файли чи отримує доступ до БД, тестування цих частин ізольовано та знущання над ними чи розпочате тести, створюючи структури даних, прочитані з файлу / бази даних - очевидне місце для початку. Насправді створення інфраструктури тестування займе набагато більше часу, ніж написання набору швидких і брудних тестів; і кожного разу, коли ви запускаєте двохвилинний набір тестів на інтеграцію, а не витрачаючи 30 хвилин вручну на тестування частини того, що охоплювали тести інтеграції, ви вже отримуєте великий виграш.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.