Навіщо писати тести на код, який я буду рефактор?


15

Я перетворюю величезний клас спадкового коду. Рефакторинг (я вважаю) виступає за це:

  1. писати тести для спадкового класу
  2. рефакторинг хека з класу

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

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

Це справжня причина писати тести перед рефактором - щоб допомогти мені зрозуміти код краще? Має бути ще одна причина!

Будь ласка, поясніть!

Примітка:

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


1
Ваше приміщення неправильне. Ви не зміните свої тести. Ви напишете нові тести. Крок 3 буде "видалити будь-які тести, які зараз не працюють".
pdr

1
На кроці 3 можна прочитати "Написати нові тести. Видалити неіснуючі тести". Я думаю, що це все ще означає руйнування оригінальної роботи
Денніс

3
Ні, ви хочете написати нові тести під час кроку 2. І так, крок 1 знищений. Але це було марною тратою часу? Ні, тому що це дає вам впевненість у тому, що ви нічого не порушите під час кроку 2. Ваші нові тести не відповідають.
pdr

3
@Dennis - хоча я поділяю багато тих самих проблем, які ви маєте щодо ситуацій, ми можемо вважати, що більшість зусиль, що рефакторингу, є "знищенням оригінальної роботи", але якби ми ніколи її не знищували, ми ніколи не відходимо від коду спагетті з 10-ти рядковими рядками в одному файл. Можливо, те саме слід пройти для одиничних тестів, вони йдуть рука об руку з кодом, який вони тестують. З розвитком коду і переміщення та / або видалення речей, так само з ним повинні змінюватися одиничні тести.
DXM

"Розуміння коду" - не мала перевага. Як ви очікуєте переробити програму, яку ви не розумієте? Це неминуче і який кращий спосіб продемонструвати справжнє розуміння програми, ніж написати ретельний тест. Крім того, слід сказати, що чим абстрактніше тестування, тим менше ймовірність, що вам доведеться його подряпати пізніше, тому, якщо що завгодно, спочатку дотримуйтесь тестування високого рівня.
Ніл

Відповіді:


46

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

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

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

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


6
+1. Прочитайте мою думку, написали мою відповідь. Важливий момент: вам може знадобитися написати одиничні тести, щоб показати, що ті самі помилки все ще є після рефакторингу!
david.pfx

Питання: чому у прикладі зміни назви функції ви спочатку змінюєте тест, щоб переконатися, що він не працює? Хочу сказати, що, звичайно, він вийде з ладу, коли ви його зміните - ви порушили з’єднання, яке використовують лінкери, щоб зв'язати код разом! Ви, напевно, очікуєте, що може бути ще одна приватна функція за тільки що вибраним вами іменем, і ви повинні переконатися, що це не так, якщо ви її пропустили? Я бачу, що це дасть вам певну впевненість, що межує з OCD, але в цьому випадку це відчувається як надмірне вбивство. Чи коли-небудь можлива причина, що тест у вашому прикладі не провалиться?
Денніс

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

@ Денніс насправді, я несвідомо описував науково правильний експеримент: ми не можемо сказати, який параметр насправді вплинув на результат, змінивши ще один парам. Пам'ятайте, що тести - це код, і кожен код містить помилки. Ви підете в пекло програміста, щоб не запустити тести, перш ніж торкнутися коду? Звичайно, ні: хоч тести були б ідеальними, це ваше професійне судження, чи це необхідно. Далі зауважте, що тест не вдався, якщо він не компілюється, і що моя відповідь також застосовна до динамічних мов, а не лише до статичних мов з лінкером.
амон

2
З виправлення різних помилок під час рефакторингу я розумію, що я б не робив так, щоб переміщення коду було так легко, не маючи тестів. Тести попереджають мене про поведінкові / функціональні "відмінності", які я запроваджую, змінюючи код.
Денніс

7

Ах, збереження спадкових систем.

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

Редагувати. Скажімо, частина вашого коду щось вимірює, і він має функцію, яка просто повертає значення. Єдиний інтерфейс викликає функцію / метод / що-небудь і отримує повернене значення. Це нещільне з’єднання та просте тестування в одиниці. Якщо у вашій головній програмі є підкомпонент, який управляє буфером, і всі виклики до нього залежать від самого буфера, деяких керуючих змінних, і він відкидає повідомлення про помилки через інший розділ коду, то ви можете сказати, що це щільно пов'язане, і це важкий для одиниці тест. Ви все ще можете робити це з достатньою кількістю знущаються предметів і нічого подібного, але це стає безладним. Особливо в c. Будь-яка кількість рефакторингу, як працює буфер, порушить підкомпонент.
Завершити редагування

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

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

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

Крім того, трактувати знання спадкової системи як "тимчасові знання" не вдасться. Знання того, як це робилося раніше, це дуже важливо, коли мова йде про застарілі системи. Надзвичайно корисний для вікових питань питання "чому, чорт, це робить?"


Я думаю, що я розумію, але ти втратив мене на інтерфейсах. тобто тести, які я зараз пишу, перевірте, чи певні змінні були заповнені належним чином, після виклику методу тестування. Якщо ці змінні будуть змінені або відремонтовані, це будуть мої тести. Існуючий спадковий клас, з яким я працюю, не має інтерфейсів / getters / setters per seh, які могли б змінити зміни або такі менш трудомісткі. Але я знову не впевнений, що ви маєте на увазі під інтерфейсами, коли мова йде про застарілий код. Можливо, я можу створити якісь? Але це буде рефакторинг.
Денніс

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

4

Моя відповідь / реалізація:

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

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

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

ОНОВЛЕННЯ

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

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


Звучить, як ви переробили додаток разом із рефакторингом.
JeffO

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

3

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

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

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


Ваш перший абзац, як видається, виступає за ігнорування кроку 1 та написання тестів, як він іде; здається, ваш другий абзац суперечить цьому.
pdr

Оновлено мою відповідь.
Майкл Дюррант

2

Яка мета рефакторингу у вашому конкретному випадку?

Припустімо, щоб миритись з моєю відповіддю, що всі ми (певною мірою) віримо в TDD (Test-Driven Development).

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

  • Тести допоможуть вам переконатися, що ваша нова робота справді працює.

  • Тести, ймовірно, також виявлять випадки, коли оригінальний твір не працює.

Але як же насправді зробити якийсь значний рефакторинг, не впливаючи на поведінку певною мірою?

Ось короткий список кількох речей, які можуть статися під час рефакторингу:

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

Я хочу стверджувати, що кожна з перерахованих видів діяльності певним чином змінює поведінку .

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

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

Як щодо цього сценарію:

  • Припустимо, у вас є 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()було занадто складно, щоб почати з цього і його слід розбити. Але процедури здатні викликати інші процедури з причини і неможливо усунути всю складність, правда? Наша робота - керувати складністю розумно.

Розглянемо інший сценарій.

Ви будуєте будівлю.

Ви будуєте ліси, щоб забезпечити правильність будівлі будівлі.

Ліси допомагають вам побудувати шахту ліфта, серед іншого. Після цього ви зриваєте ліси, але вал ліфта залишається. Ви знищили "оригінальну роботу", знищивши риштування.

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

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