Чи погана практика змінювати код строго для тестування


77

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

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

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

Просто простий приклад, придумайте цей фрагмент коду, який використовується деяким компонентом (написаний на C #):

public void DoSomethingOnAllTypes()
{
    var types = Assembly.GetExecutingAssembly().GetTypes();

    foreach (var currentType in types)
    {
        // do something with this type (e.g: read it's attributes, process, etc).
    }
}

Я припустив, що цей код можна змінити, щоб викликати інший метод, який виконає фактичну роботу:

public void DoSomething(Assembly asm)
{
    // not relying on Assembly.GetExecutingAssembly() anymore...
}

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

Що вважається хорошою і поширеною практикою?


5
Ваш змінений метод повинен приймати Typeяк параметр, а не an Assembly.
Роберт Харві

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

5
У вашому автомобілі є порт ODBI для "тестування" - якби речі були ідеальними, він би не знадобився. Я здогадуюсь, ти автомобіль надійніший за його програмне забезпечення.
mattnz

24
Яка впевненість у нього, що код "працює"?
Крейдж

3
Звичайно - зроби так. Випробуваний код стабільний у часі. Але у вас буде набагато простіший час пояснити нещодавно внесені помилки, якщо вони прийшли з деяким поліпшенням функції, а не з виправленням перевірки
Petter Nordlander,

Відповіді:


135

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

  • Легше обслуговувати,
  • Простіше міркувати,
  • Більш вільно пов'язане, і
  • Архітектурно має кращий дизайн.

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

2
+1: Я взагалі згоден. Безумовно, є чіткі випадки, коли тестування коду завдає шкоди цим іншим корисним цілям, і в цих випадках перевіряемость лежить на найнижчому рівні. Але в цілому, якщо ваш код недостатньо гнучкий / розширюваний для тестування, він не буде достатньо гнучким / розширюваним для використання або витонченого віку.
Теластин

6
Тестовий код - краще код. Завжди є властивий ризик зміни коду, який не має тестів навколо нього, але найпростіший, найдешевший спосіб пом'якшити цей ризик в надзвичайно короткий термін - це залишити його в спокої, що добре ... поки вам насправді не потрібно щоб змінити код. Що потрібно зробити, - продати переваги одиничного тестування своєму колезі; якщо всі є на борту з одиничним тестуванням, то не може бути аргументу того, що код потрібно перевірити. Якщо не всі борту з одиничним тестуванням, немає сенсу.
guysherman

3
Саме так. Пам’ятаю, я писав одиничні тести для близько 10 к рядків власного коду. Я знав, що код працює чудово, але тести змусили мене переосмислити деякі ситуації. Мені довелося запитати себе «що саме робить цей метод?». Я знайшов кілька помилок у робочому коді, лише переглянувши його з нової точки зору.
Султан

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

59

У грі є (начебто) протилежні сили .

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

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

static void Main(string[] args)

Ваш колега пропонує зробити це єдиною точкою доступу у вашому коді? Чи повинні всі інші коди недоступні зовнішнім абонентам?

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

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

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

Відкриття API для перевірки достовірності - це те , що ви дійсно робите, це застосовувати принцип відкритості / закритості .

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

Таким чином, якщо тести пропонують вам відкрити свій API, тоді зробіть це; але підтримуйте інкапсуляцію не приховуючи складності, а виявляючи складність безвідмовно.


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

1
Я люблю вашу відповідь! :) Скільки разів я чую: "Інкапсуляція - це процес приватних деяких методів та властивостей". Це як сказати, коли ви програмуєте в об'єктно-орієнтованому, це тому, що ви програмуєте з об'єктами. :( Я не здивований, що настільки чиста відповідь приходить від МАЙСТЕРА ВПЛІВЛЕННЯ ВІДОМОСТІ. Я, безумовно, кілька разів прочитаю вашу відповідь, щоб змусити мене посміхнутися моєму незадовільному старому коду.
Самуїл

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

21

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

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


DI не є головним питанням (я лише намагався навести приклад), справа в тому, чи було б нормально запропонувати інший метод, який спочатку не передбачався створювати, лише заради тестування.
liortal

4
Я знаю. Я подумав, що я звернувся до вашого основного пункту у другому абзаці з "так".
Jason Swett

13

Це трохи проблема курки та яєць.

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

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

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

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

Звичайно, чи це хороша ділова практика - це ціла «велика банка глистів».


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

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

7

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

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

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

(Звичайно, новий код активно підтримується, тому його слід писати, щоб бути перевіреним.)


2

Я думаю, що ваш колега помиляється.

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

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

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


2

Я використовував інструменти покриття коду як частину тестування одиниць, щоб перевірити, чи виконуються всі шляхи через код. Як досить хороший кодер / тестер самостійно, я зазвичай охоплюю 80-90% кодових шляхів.

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


2

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

Багато людей змінили свій код, щоб запровадити класи IoC, DI та інтерфейси, щоб увімкнути тестування одиниць, використовуючи інструменти глузування та тестування модулів, які потребують цих змін коду. Мені не здається, що це здорова річ, не коли ви бачите код, який був досить прямим і простим, перетворювався на кошмарний безлад складних взаємодій, зумовлений цілком необхідністю, щоб кожен метод класу повністю відключався від усього іншого . І щоб додати образи до травми, у нас тоді є багато аргументів щодо того, чи варто приватні методи перевіряти чи ні! (звичайно, вони повинні, що "

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

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

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

У статті Мартіна Фаулера дискусія навколо цієї сфери є знущаннями, а не глушниками


1

Доброю і поширеною практикою є використання тестування блоків та журналів налагодження . Блок тестів гарантує, що якщо ви внесете будь-які подальші зміни в програму, ваша стара функціональність не порушиться. Журнали налагодження можуть допомогти відстежити програму під час виконання.
Іноді трапляється, що навіть поза цим нам потрібно мати щось лише для тестування. Для цього незвично змінювати код. Але слід бути обережним, щоб виробничий код не вплинув через це. У C ++ і C це досягається за допомогою MACRO , який є об'єктом часу компіляції. Тоді код тестування взагалі не враховується у виробничих умовах. Не знаю, чи є таке положення у C #.
Крім того, коли ви додаєте тестовий код у свою програму, він повинен бути добре виднощо ця частина коду додається з якоюсь метою тестування. Або ж розробник, який намагається зрозуміти код, просто пітніє над тією частиною коду.


1

Є кілька серйозних відмінностей між вашими прикладами. У випадку з DoSomethingOnAllTypes()цим є наслідки, які do somethingзастосовні до типів у поточній збірці. Але DoSomething(Assembly asm)явно вказує на те, що можна пройти будь-яку збірку.

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


0

Ваше запитання дало не багато контексту, в якому ваш колега сперечався, щоб було місце для спекуляцій

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

На мій погляд, ваш приклад вилучення методу DoSomething(types)- це нормально.

Але я бачив код, який не в порядку, як це:

public void DoSomethingOnAllTypes()
{
  var types = (!testmode) 
      ? Assembly.GetExecutingAssembly().GetTypes() 
      : getTypesFromTestgenerator();

  foreach (var currentType in types)
  {
     if (!testmode)
     {
        // do something with this type that made the unittest fail and should be skipped.
     }
     // do something with this type (e.g: read it's attributes, process, etc).
  }
}

Ці зміни зробили код більш зрозумілим, оскільки ви збільшили кількість можливих кодових шляхів.

Що я маю на увазі як і коли :

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

if (!testmode)Більш більш difficulilt зрозуміти і тест , ніж витягнутий метод.

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