Як одиничні тести полегшують дизайн?


43

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

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



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

4
@gnat тестування одиниць не означає автоматично TDD, це вже інше питання
Joppe

11
"тестова одиниця (перевірка значень у полях") - ви, здається, поєднуєте тестові одиниці з перевіркою введення.
jonrsharpe

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

Відповіді:


3

Не тільки тестування приладів полегшують дизайн, але це одна з їх головних переваг.

Написання тесту спочатку виключає модульність та чисту структуру коду.

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

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

Потім, як тільки цей пристрій поводиться як зазначено, ви переходите до написання виявлених залежностей (для xта y).

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

Написання тестів пізніше підкаже, коли ваш код погано структурований.

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

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

Коли ваші сценарії стають занадто складними ( «якщо xі yі zпотім ...») , тому що вам потрібно абстрагувати більше, ви знаєте , у вас є поліпшення , щоб зробити в вашому коді.

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

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


@SSECmmunity: На сьогоднішній день лише 2 оновлення, цю відповідь дуже легко не помітити. Я настійно рекомендую розмову Майкла Пір'я, яка була пов'язана у цій відповіді.
displayName

103

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

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


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

41
На мій досвід, більшість одиничних тестів не "використовують ваш код, як інші програмісти будуть використовувати ваш код." Вони використовують ваш код, оскільки одиничні тести будуть використовувати код. Щоправда, вони виявлять багато серйозних недоліків. Але API, призначений для тестування одиниць, може бути не тим API, який найбільш підходить для загального використання. Спрощено написані одиничні тести часто вимагають, щоб базовий код виявив занадто багато внутрішніх. Знову ж таки, виходячи з мого досвіду - було б цікаво почути, як ви з цим впоралися. (Дивіться мою відповідь нижче)
user949300

7
@ user949300 - Я не дуже вірю в тест першим. Моя відповідь спочатку ґрунтується на ідеї коду (і, звичайно, дизайну). API не повинні розроблятися для тестування одиниць, вони повинні бути розроблені для вашого клієнта. Тестові одиниці допомагають наблизити клієнта, але вони є інструментом. Вони там, щоб служити вам, а не навпаки. І вони, звичайно, не збираються зупиняти вас у створенні шаленого коду.
Теластин

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

2
@ user949300 - класичний приклад, про який я мав на увазі, це Репозиторій, який потребує connString. Припустимо, ви розкриєте це як загальнодоступне записуване майно та маєте встановити його після того, як ви створили новий () репозиторій. Ідея полягає в тому, що після 5-го чи шостого разу ви написали тест, який забуває зробити цей крок - і, таким чином, вийде з ладу - ви будете "природно" схильні до того, щоб змусити connString бути інваріантом класу - передавши в конструктор - тим самим зробивши свій API краще, і це робить більш імовірним, що виробничий код може бути записаний, що дозволяє уникнути цієї пастки. Це не гарантія, але це допомагає, imo.
Стівен Берн

31

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

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

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

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


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

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

@AntP Так, це частина дизайну API.
Thorbjørn Ravn Andersen

@JuanMendes Це не рідкість, і ці тести потрібно буде змінити, як і будь-який інший код, коли ви змінюєте вимоги. Хороший ІДЕ допоможе вам переробити класи в рамках роботи, яка виконується автоматично, коли ви змінюєте підписи методу тощо.
Thorbjørn Ravn Andersen

@JuanMendes, якщо ви пишете хороші тести та невеликі одиниці, вплив ефекту, який ви описуєте, на практиці незначний.
Мураха P

6

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

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

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

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


Велика кількість параметрів методу - кодовий запах та недолік дизайну.
Суфіан

5

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

Після розробки програми (з одиничними тестами) після виявлення помилок ви можете додати тести, щоб підтвердити виправлення помилок.

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

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


Це чудова відповідь для мене. Ви б проти занести кілька прикладів, наприклад, по одному для кожного випадку (коли ви ознайомитесь із дизайном тощо).
User039402

5

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

Це автоматично змусить вас відокремити читання рядків від читання окремих значень.

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

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


4

Простіше кажучи, написання одиничних тестів допоможе виявити недоліки у вашому коді.

Цей вражаючий посібник із написання тестового коду , написаний Джонатаном Волтером, Рассом Руффером та Мішко Гевери, містить численні приклади того, як недоліки в коді, які, як правило, гальмують тестування, також запобігають легкому повторному використанню та гнучкості того ж коду. Таким чином, якщо ваш код перевіряється, його легше використовувати. Більшість "моралі" - це смішно прості поради, які значно покращують дизайн коду ( Dependency Injection FTW).

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

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Однак, коли ми використовуємо ін'єкцію залежності, набагато простіше перевірити, чи метод computeStuff працює належним чином, коли кеш починає виселяти речі. Все, що ми робимо, - це створити тест, у якому ми зателефонуємо new HereIUseDI(buildSmallCache()); Notice, у нас є більш нюансований контроль над об’єктом, і він одразу виплачує дивіденди.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

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


2
Чесно кажучи, я не впевнений, як ви маєте на увазі приклад. Як метод computeStuff стосується кеша?
Іван V

1
@ user970696 - Так, я маю на увазі, що "computeStuff ()" використовує кеш. Питання "Чи правильно працює computeStuff () весь час (що залежить від стану кешу)" Отже, важко підтвердити, що computeStuff () робить те, що ви хочете, для ВСІХ МОЖЛИВИХ КАТЕЙСЬКИХ ДЕРЖАВ, якщо ви не можете безпосередньо встановити / побудувати кеш, тому що ви жорстко кодували рядок "cacheOfExpensiveComputation = buildBigCache ();" (на відміну від переходу в кеш безпосередньо через конструктор)
Іван

0

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

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

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

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

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


-2

Експериментальні тести можуть допомогти у рефакторингу, коли новий код пройде всі старі тести.

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

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


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

@AntP, ОП запитала про тести рефакторингу та одиниці.
om

1
Питання згадувало рефакторинг, але власне питання полягало у тому, як тести для одиниць можуть покращити / перевірити дизайн коду - не полегшити сам процес рефакторингу.
Мураха P

-3

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


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