Чи є сенс одиничних тестів, які заглушають і знущаються над усім загальнодоступним?


59

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

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


4
Мої два центи: Не зловживайте глузуванням
Хуан Мендес

1
Дивіться також "
Знущання над запахом

Відповіді:


37

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

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

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

Що мені здалося корисним - це розділити такі класи на:

  1. Заняття з фактичною "діловою логікою". Вони використовують мало дзвінків або взагалі не звертаються до інших класів і їх легко перевірити (значення (значення)).
  2. Класи, що взаємодіють із зовнішніми системами (файлами, базами даних тощо). Вони обгортають зовнішню систему та забезпечують зручний інтерфейс для ваших потреб.
  3. Заняття, які "пов'язують все разом"

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

Класи з 2. і 3. зазвичай не можуть бути змістовно перевірені одиницями (оскільки вони не роблять нічого корисного самостійно, вони просто "склеюють" код). ОТОГ, як правило, ці класи відносно прості (і мало), тому їх слід адекватно охопити інтеграційними тестами.


Приклад

Один клас

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

Якщо у вас все це в одному класі, вам потрібно буде зателефонувати до функцій БД, які важко висміювати. У псевдокоді:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

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

Розділяти

Ви розділилися на три класи: ціновий розрахунок, ціновий репозиторій, додаток.

PriceCalculation робить лише фактичний розрахунок і отримує необхідні значення. Додаток пов'язує все разом:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

Цей шлях:

  • PriceCalculation включає в себе "логіку бізнесу". Це легко перевірити, оскільки він нічого не називає самостійно.
  • PriceRepository можна перевірити псевдо-одиницею, встановивши макетну базу даних і протестуючи дзвінки для читання та оновлення. У ньому мало логіки, отже, мало кодових шляхів, тому вам не потрібно занадто багато цих тестів.
  • Додаток не може бути тестовано одиничним, тому що це клей-код. Однак це теж дуже просто, тому тестування на інтеграцію має бути достатньо. Якщо пізніше додаток стане занадто складним, ви вийдете з класів «бізнес-логіка».

Нарешті, може виявитися, що PriceCalculation повинен робити власні дзвінки до бази даних. Наприклад, тому що лише PriceCalculation знає, в яких даних є її потреби, тому додаток не може заздалегідь отримати їх. Потім ви можете передати йому екземпляр PriceRepository (або якийсь інший клас репозиторію), пристосований до потреб PriceCalculation. Потім цей клас потрібно буде глузувати, але це буде просто, оскільки інтерфейс PriceRepository простий, наприклад PriceRepository.getPrice(articleNo, contractType). Найголовніше, що інтерфейс PriceRepository ізолює PriceCalculation від бази даних, тому зміни в схемі БД або організації даних навряд чи змінять її інтерфейс, а отже, і зруйнують макети.


5
Я думав, що я один не бачу сенсу в одиничному тестуванні всього, дякую
enthrops

4
Я просто не погоджуюся, коли ти кажеш, що класів типу 3 мало, я відчуваю, що більшість мого коду типу 3 і майже немає логіки бізнесу. Це те, що я маю на увазі: stackoverflow.com/questions/38496185/…
Родріго Руїз

27

Що є вирішальною перевагою тестування одиниць проти інтеграційного тестування?

Це помилкова дихотомія.

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

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

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

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

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


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

Якщо ви витрачаєте багато часу на написання одиничних тестів на кшталт тривіального коду

public string SomeProperty { get; set; }

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

public string SomeMethod(string someProperty);

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


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

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

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

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

1
Мій досвід створення безлічі тестів на інтеграцію та інтеграцію (не кажучи вже про вдосконалені знущальні, інтеграційні тестування та засоби кодового покриття, які використовуються цими тестами) суперечить більшості ваших претензій тут: 1) "Мета тестування одиниць полягає в тому, щоб переконатися, що ваш код робить те, що належить ": те саме стосується тестування інтеграції (тим більше); 2) "Одиничні тести налаштовані набагато простіше": ні, вони не є (досить часто, тести інтеграції простіші); 3) "При правильному використанні одиничні тести заохочують розробку" перевіряемого "коду: те саме, що і тести інтеграції; (продовжується)
Rogério

4

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

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

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

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

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

ОНОВЛЕННЯ

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

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


І якщо метод не називає нічого приватним, то немає сенсу тестувати його, правильно?
захоплює

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

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

Так. Метод щось робить, чи не так? Тож його слід перевірити. З точки зору тестування, я не знаю, чи використовується щось приватне. Я просто знаю, що якщо я надам вхід А, я повинен отримати вихід Б.
Шлейс

О так, метод щось робить, і щось викликає інші публічні методи (і тільки це). Тож те, як ви «належним чином» тестуєте, що заглушує виклики з деякими значеннями повернення, а потім налаштовує очікування повідомлень. Що ТИЧНО ви тестуєте в цьому випадку? Що правильно робити дзвінки? Ну, ви написали цей метод, і можете подивитися на нього і побачити, що саме він робить. Я думаю, що тестування блоків є більш підходящим для ізольованих методів, які повинні використовуватися як "вхід -> вихід", тому ви можете встановити купу прикладів, а потім зробити регресійне тестування, коли рефактор.
захоплює

3

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

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

Насправді, надмірне глузування є анти-закономірністю: TDD Anti-Patterns та Mocks - це зло .


0

Хоча оп вже позначив відповідь, я тут просто додаю свої 2 центи.

Яка вирішальна перевага модульного тестування та інтеграційного тестування (за винятком витрат на час)?

А також у відповідь на

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

Є корисна, але не зовсім те, що просила ОП:

Тестові одиниці працюють, але помилок все ще є?

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

  1. не створюйте одиничне тестування для приватного методу.
  2. створити блок-тести для приватного методу.

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


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

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

Якби я обрав підхід 2: код, написаний для одиничних тестів, був би порівняно меншим, і це було б набагато простіше перевірити.


Враховуючи випадок, коли приватний метод не використовується повторно, для цього методу немає сенсу писати окремий одиничний тест.

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

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