Як ви підтримуєте тести своїх пристроїв працювати при рефакторингу?


29

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

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

Як уникнути порушення тестів при рефакторингу?

  • Ви пишете тести «краще»? Якщо так, то на що слід звернути увагу?
  • Чи уникаєте ви певних видів рефакторингу?
  • Чи є інструменти для рефакторингу?

Редагувати: я написав нове запитання, в якому запитав, що я маю намір запитати (але це було цікавим варіантом).


7
Я б подумав, що, використовуючи TDD, ваш перший крок у рефакторингу - це написати тест, який не працює, а потім перефактуруйте код, щоб він працював.
Метт Еллен

Чи не можете ваш IDE розібратися, як перефактурувати тести?

@ Thorbjørn Ravn Andersen, так, і я написав нове запитання, яке запитав, що я маю намір запитати (але це вважало цікавим варіантом; див. Відповідь ажеглова, що по суті те, що ви говорите)
Алекс Фейнман

Розглядали питання додавання інформації до цього питання?

Відповіді:


35

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

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

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


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

3
Якщо тести потребують постійного коригування, це, мабуть, натяк на наявність занадто детальних тестів. Наприклад, припустимо, що фрагмент коду повинен викликати події A, B і C за певних обставин, не в певному порядку. Старий код робить це для того, щоб ABC і тести очікували подій у тому порядку. Якщо кодекс, що реконструюється, виганяє події для того, щоб ACB все ще працював відповідно до специфікації, але тест не вийде.
otto

3
@Kevin: Я вважаю, що те, що ви описуєте, є перепроектуванням, оскільки публічний інтерфейс змінюється. Визначення Фаулера щодо рефакторингу ("зміна [коду] внутрішньої структури без зміни зовнішньої поведінки") досить чітко про це.
ажеглов

3
@azheglov: можливо, але, на мій досвід, якщо реалізація погана, то інтерфейс
Кевін

2
Цілком правильне і чітке запитання закінчується дискусією про «значення слова». Кого хвилює, як ви це називаєте, давайте обговоримо десь ще. Тим часом ця відповідь повністю опускає будь-яку реальну відповідь, але все ще має найбільше відгуків. Я розумію, чому люди відносять TDD до релігії.
Дірк Бур

21

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

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

Якщо рефакторинг змінює загальнодоступний інтерфейс, то тести слід переписати спочатку. Рефактор, поки не пройдуть нові тести.

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


7

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

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

Якщо я перевіряю внутрішній стан мого SUT, відкриваючи його приватних або захищених членів (ми могли б використовувати "товариша" у візуальному базовому або збільшити рівень доступу "внутрішній" і використовувати "Internalsvisibleto" в c #; багатьма мовами OO, в т.ч. c # " тест-специфічний підклас " може бути використаний) тоді раптом внутрішній стан класу матиме значення - ви, можливо, будете переробляти клас як чорний ящик, але тести білого поля не зможуть. Припустимо, що одне поле повторно використовується для позначення різних речей (не належна практика!), Коли стан SUT змінюється - якщо ми розділимо його на два поля, нам може знадобитися переписати зламані тести.

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

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

Таким чином, щоб запобігти поєднанню тестів з інтимними внутрішніми деталями SUT, це може допомогти:

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

Усі вищезазначені моменти - приклади з’єднання білих коробок, використовуваних у тестах. Отже, щоб повністю уникнути тесту на тесту на рефакторинг, використовуйте тестування на SUT в чорному ящику.

Відмова: Для обговорення рефакторингу тут я використовую слово трохи ширше, щоб включити зміни внутрішньої реалізації без видимих ​​зовнішніх ефектів. Деякі пуристи можуть не погодитись і посилаються виключно на книгу Мартіна Фаулера та Кента Бека «Рефакторинг», де описані операції рефакторингу атомів.

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

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

[...]

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

Фаулер - макети не заглушки


Фоулер буквально написав книгу про Рефакторинг; і найавторитетніша книга про тестування модулів (xUnit Test Patterns від Джерарда Мессароса) знаходиться у серії "підпису" Фоулера, тому, коли він каже, що рефакторинг може зламати тест, він, мабуть, правий.
перфекціоніст

5

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

Іноді НЕ потрібно змінювати поведінку ваших тестів. Можливо, вам потрібно об'єднати два способи разом (скажімо, bind () та прослухати () на прослуховуючий клас TCP-сокету), тому у вас є інші частини коду, які намагаються та не можуть використовувати тепер змінений API. Але це не рефакторинг!


Що робити, якщо він просто змінить назву методу, перевіреного тестами? Тести не вдасться, якщо ви також не переймените їх у тести. Тут він не змінює поведінку програми.
Оскар Медерос

2
У такому разі його тести також реконструюються. Потрібно бути обережним: спочатку ви перейменовуєте метод, а потім запускаєте тест. Він повинен провалитися з правильних причин (він не може компілювати (C #), ви отримуєте виняток MessageNotUnder зрозумів (Smalltalk), нічого, схоже, не трапиться (нульовий зразок Objective-C)). Потім ви змінюєте тест, знаючи, що ви випадково не ввели жодної помилки. "Якщо ваші тести перервані" означає "якщо ваші тести перерються після того, як ви закінчили рефакторинг", іншими словами. Спробуйте зберегти невеликі шматки змін!
Френк Шірар

1
Тестові одиниці притаманні структурі коду. Наприклад, Fowler має багато в refactoring.com/catalog, які впливали б на тести одиниць (наприклад, метод приховати, метод вбудованого, замінити код помилки на виняток тощо).
Крістіан Н

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

Я написав вище, припускаючи, що добре написані тести: якщо ви тестуєте свою реалізацію - якщо структура тесту відображає внутрішню частину тестового коду, обов'язково. У такому випадку перевіряйте контракт підрозділу, а не реалізацію.
Френк Ширар

4

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

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

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

Але це досить очевидно. Таким чином, ви ПРОБЛЕЖНО означаєте, що рефакторинг, що ви змінюєте API.

Тож дозвольте мені відповісти, як підійти до цього!

  • Спочатку створіть НОВИЙ API, який виконує те, що ви хочете, щоб ваше поведінка NEW API. Якщо трапляється, що цей новий API має те саме ім'я, що і API OLDER, тоді я додаю ім'я _NEW до нового імені API.

    int DoSomethingInterestingAPI ();

стає:

int DoSomethingInterestingAPI_NEW( int takes_more_arguments );
int DoSomethingInterestingAPI_OLD();
int DoSomethingInterestingAPI() { DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API);

Гаразд - на цьому етапі всі ваші регресійні тести проходять підвіконня - використовуючи назву DoSomethingInterestingAPI ().

НАСТУПНО, перейдіть через свій код і змініть всі дзвінки на DoSomethingInterestingAPI () на відповідний варіант DoSomethingInterestingAPI_NEW (). Це включає оновлення / перезапис будь-яких частин регресійних тестів, які потрібно змінити, щоб використовувати новий API.

NEXT, позначте DoSomethingInterestingAPI_OLD () як [[застаріле ()]]. Тримайтеся довше застарілого API (доки не захочете оновити весь код, який може від нього залежати).

При такому підході будь-які збої у ваших тестах регресії просто є помилками в цьому регресійному тесті або виявлення помилок у вашому коді - саме так, як ви хотіли б. Цей поетапний процес перегляду API шляхом явного створення _NEW та _OLD версій API дозволяє деякий час співіснувати біти нового та старого коду.


Мені подобається ця відповідь, оскільки це дає зрозуміти, що Тестові підрозділи для SUT є такими ж, як і зовнішні клієнти опублікованого Api. Те, що ви призначаєте, дуже схоже на протокол SemVer для управління опублікованою бібліотекою / компонентом, щоб уникнути "пекла залежності". Однак це досягає витрат часу та гнучкості, екстраполяція такого підходу до публічного інтерфейсу кожного мікроагрегата означає і екстраполяцію витрат. Більш гнучкий підхід полягає в тому, щоб якомога більше відокремити тести від впровадження, тобто інтеграційне тестування або окремий DSL для опису тестових входів та результатів
KolA

1

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

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


1
Найкорисніша відповідь, яка насправді стосується питання - не будуйте тестове покриття на хиткій основі внутрішніх дрібниць і не сподівайтесь, що воно постійно розпадається - все ж найбільш прихильне, оскільки TDD призначає робити саме навпаки. Це ви отримуєте за вказівку незручної істини щодо надмірно розкритого підходу.
KolA

1

збереження набору тестів у синхронізації з кодовою базою під час та після рефакторингу

Що ускладнює зв'язок . Будь-які тести мають певний ступінь зв’язку з деталями впровадження, але одиничні тести (незалежно від того, чи це TDD чи ні), особливо погані в цьому, оскільки вони заважають внутрішнім: більше тестів на одиницю дорівнює більше коду, пов'язаного з одиницями, тобто методами підписів / будь-яким іншим публічним інтерфейсом одиниць - як мінімум.

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

Як уникнути порушення тестів при рефакторингу? Уникайте зчеплення. На практиці це означає уникати якомога більшої кількості одиничних тестів і надавати перевагу тестам вищого рівня / інтеграції, які мають більш агресивні деталі щодо впровадження. Пам'ятайте, що, як немає срібної кулі, тести все одно повинні підключитися до чогось на якомусь рівні, але в ідеалі це повинен бути інтерфейс, який явно виконаний з використанням Semantic Versioning, тобто зазвичай на опублікованому рівні api / application (ви не хочете робити SemVer для кожної одиниці вашого рішення).


0

Ваші тести занадто щільно поєднані з реалізацією, а не з вимогою.

спробуйте написати свої тести з такими коментарями:

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

таким чином, ви не можете змінити значення сенсу тестів.

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