Коли мені слід глузувати?


138

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


Я рекомендую лише знущатися над залежностями поза процесом і лише з них, взаємодії з якими можна спостерігати зовні (SMTP-сервер, шина повідомлень тощо). Не знущайтеся над базою даних, це деталізація реалізації. Більше про це тут: enterprisecraftsmanship.com/posts/when-to-mock
Володимир

Відповіді:


122

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

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

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

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

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

TL; DR: знущайтеся над кожною залежністю, яку торкається ваш тест.


164
Ця відповідь занадто радикальна. Одиничні випробування можуть і повинні застосовувати більше, ніж один метод, до тих пір, поки це все належить до одного згуртованого підрозділу. В іншому випадку знадобиться занадто багато насмішок / підробок, що призведе до складних і крихких тестів. Тільки залежності, які насправді не належать до тестуваної одиниці, повинні бути замінені глузуванням.
Rogério

10
Ця відповідь також занадто оптимістична. Було б краще, якби вона включила @ Ян недоліки макетних об'єктів.
Джефф Аксельрод

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

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

2
TL; DR: знущайтеся над кожною залежністю, яку торкається ваш тест. - Це насправді не чудовий підхід, каже сам макет - не знущайтеся над усім. (downvoted)
p_champ

167

Обметні об'єкти корисні, коли ви хочете перевірити взаємодію між тестовим класом та певним інтерфейсом.

Наприклад, ми хочемо перевірити, що цей метод sendInvitations(MailServer mailServer)викликає MailServer.createMessage()рівно один раз, а також дзвонить MailServer.sendMessage(m)рівно один раз, а інші методи не викликаються в MailServerінтерфейсі. Це коли ми можемо використовувати макетні об’єкти.

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

Теоретично це звучить добре, але є і деякі недоліки.

Знущаються з недоліків

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

Ось приклад у псевдокоді. Припустимо, ми створили MySorterклас і хочемо перевірити його:

// the correct way of testing
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert testList equals [1, 2, 3, 7, 8]
}


// incorrect, testing implementation
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert that compare(1, 2) was called once 
    assert that compare(1, 3) was not called 
    assert that compare(2, 3) was called once 
    ....
}

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

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

Знущається як заглушки

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

Припустимо, у нас є метод, sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)який ми хочемо перевірити. PdfFormatterОб'єкт може бути використаний для створення запрошення. Ось тест:

testInvitations() {
   // train as stub
   pdfFormatter = create mock of PdfFormatter
   let pdfFormatter.getCanvasWidth() returns 100
   let pdfFormatter.getCanvasHeight() returns 300
   let pdfFormatter.addText(x, y, text) returns true 
   let pdfFormatter.drawLine(line) does nothing

   // train as mock
   mailServer = create mock of MailServer
   expect mailServer.sendMail() called exactly once

   // do the test
   sendInvitations(pdfFormatter, mailServer)

   assert that all pdfFormatter expectations are met
   assert that all mailServer expectations are met
}

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

Але що станеться пізніше, коли ми змінимо sendInvitations()або якийсь інший клас, який sendInvitations()використовує, щоб створити більш фантазійні pdfs? Наш тест несподівано провалюється, тому що тепер PdfFormatterвикликається більше методів , і ми не навчили заглушку їх очікувати. І, як правило, це не один тест, який дає збій у подібних ситуаціях, це будь-який тест, який трапляється безпосередньо чи опосередковано використовувати sendInvitations()метод. Ми повинні виправити всі ці тести, додавши більше тренувань. Також зауважте, що ми не можемо видалити методи, які більше не потрібні, оскільки ми не знаємо, який із них не потрібен. Знову ж таки це заважає рефакторингу.

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

Як це виправити? Легко:

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

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

Детальніше про недоліки знущань див. Також Об'єкти глузування: Недоліки та випадки використання .


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

1
Хіба не вся суть знущань полягає в тому, що ваші тести послідовні, що вам не доведеться турбуватися знущатися над об’єктами, реалізація яких постійно змінюється, можливо, іншими програмістами щоразу, коли ви запускаєте тест, і отримуєте стійкі результати тесту?
PositiveGuy

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

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

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

55

Практичне правило:

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


4

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

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

Чудовий подкаст на цю тему можна знайти тут


Тепер посилання спрямовується на поточний епізод, а не на передбачуваний. Чи призначений підкаст цей hanselminutes.com/32/mock-objects ?
C Перкінс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.