Рефакторинг - чи доцільно просто переписати код, доки пройдуть усі тести?


9

Нещодавно я переглядав "Всі дрібниці" від RailsConf 2014. Під час цієї розмови Санді Мец відновлює функцію, яка включає велику вкладену операцію if:

def tick
    if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
        if @quality > 0
            if @name != 'Sulfuras, Hand of Ragnaros'
                @quality -= 1
            end
        end
    else
        ...
    end
    ...
end

Перший крок - розбити функцію на кілька менших:

def tick
    case name
    when 'Aged Brie'
        return brie_tick
    ...
    end
end

def brie_tick
    @days_remaining -= 1
    return if quality >= 50

    @quality += 1
    @quality += 1 if @days_remaining <= 0
end

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

На жаль, ведучий здався невідомим, що такий підхід призвів до того, що три з чотирьох *_tickфункцій були неправильними (а інша була порожньою!). Є крайні випадки, коли поведінка *_tickфункцій відрізняється від поведінки вихідної tickфункції. Наприклад, @days_remaining <= 0в brie_tickповинен бути < 0- значить brie_tick, не працює правильно, коли викликається з days_remaining == 1і quality < 50.

Що тут пішло не так? Це невдача тестування - бо не було тестів для цих конкретних кращих випадків? Або невдача рефакторингу - тому що код повинен був перетворюватися поетапно, а не переписуватись з нуля?


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

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

@JohnWu - У мене було враження, що рефакторинг, як правило, робиться як серія невеликих перетворень вихідного коду ("метод вилучення" тощо), а не просто переписування коду (під яким я маю на увазі написання його знову з нуля, навіть розглядаючи наявний код, як це зроблено у пов'язаній презентації).
користувач200783

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

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

Відповіді:


11

Це невдача тестування - бо не було тестів для цих конкретних кращих випадків? Або невдача рефакторингу - тому що код повинен був перетворюватися поетапно, а не переписуватись з нуля?

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

Однак іноді, переписуючи розділ коду, це те, що вам потрібно або хочете зробити. І якщо це так, вам потрібні кращі тести.

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


1
Дякую, це має сенс. Отже, якщо остаточним рішенням небажаних змін у поведінці є проведення всебічних тестів, чи є якийсь спосіб бути впевненим, що тести охоплюють усі можливі випадкові випадки? Наприклад, можна було б мати 100% охоплення, brie_tickпоки ще ніколи не тестувати проблемний @days_remaining == 1випадок, наприклад, тестуючи @days_remainingвстановлені на 10та -10.
користувач200783

2
Ви ніколи не можете бути абсолютно впевненими, що тести охоплюють усі можливі випадкові випадки, оскільки це неможливо перевірити на всіх можливих введеннях. Але існує маса способів отримати більшу впевненість у тестах. Ви можете вивчити мутаційне тестування - це спосіб перевірити ефективність тестів.
bdsl

1
У цьому випадку пропущені гілки могли бути спіймані інструментом покриття коду під час розробки тестів.
cbojar

2

Що тут пішло не так? Це невдача тестування - бо не було тестів для цих конкретних кращих випадків? Або невдача рефакторингу - тому що код повинен був перетворюватися поетапно, а не переписуватись з нуля?

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

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

З бесіди :

Тепер це справжній рефакторинг згідно з визначенням рефакторингу; Я збираюся змінити цей код. Я збираюся змінити його устрій, не змінюючи його поведінки.

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

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

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

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

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

Тому я вважаю, що ваш зв'язок обраний погано; ANDні OR.


2

Рефакторинг не повинен змінювати зовнішню поведінку вашого коду. Це і є мета.

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

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


1

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

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

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