Коли використовувати Mockito.verify ()?


201

Я пишу тестові приклади jUnit для 3 цілей:

  1. Щоб мій код відповідав усім необхідним функціоналам, під усіма (або більшістю) вхідними комбінаціями / значеннями.
  2. Щоб переконатися, що я можу змінити реалізацію, і покладатися на тестові випадки JUnit, щоб сказати мені, що всі мої функціональні можливості все ще задоволені.
  3. Як документація всіх випадків використання мій код обробляє, і він виступає як специфікація для рефакторингу - якщо код коли-небудь потрібно буде переписувати. (Код Refactor, і якщо мої тести jUnit не вдається - ви, мабуть, пропустили певний випадок використання).

Я не розумію, чому і коли Mockito.verify()слід використовувати. Коли я бачу, verify()як мене викликають, це говорить мені про те, що мій jUnit усвідомлює реалізацію. (Таким чином, зміна моєї реалізації призведе до порушення моїх jUnits, навіть якщо на мою функціональність це не вплинуло).

Я шукаю:

  1. Які повинні бути вказівки щодо відповідного використання Mockito.verify()?

  2. Чи принципово правильно, щоб jUnits усвідомлював або щільно поєднувався з реалізацією тестуваного класу?


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

Відповіді:


78

Якщо контракт класу A включає той факт, що він викликає метод B об'єкта типу C, то слід перевірити це, зробивши макет типу C і перевіривши, що метод B був викликаний.

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

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

Оновлення:

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

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

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


8
Спасибі, Девіде. Після сканування через деякі набори коду, це здається звичайною практикою - але для мене це перемагає мету створення одиничних тестів і просто додає накладні витрати на їх підтримання за дуже мало значення. Я розумію, для чого потрібні макети та чому потрібно встановити залежності для виконання тесту. Але підтвердження того, що виконується залежність методуA.XYZ (), робить тести дуже крихкими, на мою думку.
Рассел

@Russell Навіть якщо "тип C" - це інтерфейс для обгортки навколо бібліотеки чи навколо якоїсь окремої підсистеми вашої програми?
Давуд ібн Карім

1
Я б не сказав, що цілком марно забезпечувати посилання на якусь підсистему чи послугу - просто, що навколо неї мають бути деякі вказівки (формулювання їх було те, що я хотів зробити). Наприклад: (я, мабуть, надмірно простою) Скажіть, я використовую StrUtil.equals () у своєму коді та вирішую перейти до StrUtil.equalsIgnoreCase () у впровадженні. Якщо jUnit перевірив (StrUtil.equals ), мій тест може не вдатися, хоча реалізація точна. Цей виклик підтвердження, IMO, є поганою практикою, хоча це стосується бібліотек / підсистем. З іншого боку, використання підтвердження для забезпечення виклику для закриттяDbConn може бути дійсним шаблоном використання.
Рассел

1
Я вас розумію і повністю з вами згоден. Але я також вважаю, що написання інструкцій, які ви описуєте, може перетворитись на написання цілого навчального посібника з TDD або BDD. Якщо взяти ваш приклад, дзвонити equals()або equalsIgnoreCase()ніколи не буде те, що було визначено вимогами класу, тому ніколи б не було одиничного тестування. Однак "закриття з'єднання БД після завершення" (що б це не означало з точки зору впровадження) може бути вимогою класу, навіть якщо це не "бізнес-вимога". Для мене це зводиться до відносин між контрактом ...
Давуд ібн Карім

... класу, як це виражено в його бізнес-вимогах, і набір методів тестування, що одиниці тестують цей клас Визначення цього взаємозв'язку було б важливою темою у будь-якій книзі про TDD або BDD. Хоча хтось із команди Mockito міг написати публікацію на цю тему для своєї вікі, я не бачу, як це відрізнятиметься від багатьох інших доступних літератур. Якщо ви бачите, як це може відрізнятися, повідомте мене, і, можливо, ми зможемо над цим працювати разом.
Давуд ібн Карім

60

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

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

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

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

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


29

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

  • Чи слід використовувати Mockito.verify () для моєї інтеграції (або будь-якого іншого тестування, що не перевищує одиниці)?
  • Чи варто використовувати Mockito.verify () під час тестування модулів у чорному ящику ?
  • Чи варто використовувати Mockito.verify () під час мого тестування блоку білого поля ?

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

Тепер переглянемо все це покроково.

* - Чи повинен я використовувати Mockito.verify () в моїй інтеграції (або будь-яке інше тестування, яке не перевищує одиницю)? Я думаю, що відповідь однозначно ні, більше того, ви не повинні використовувати для цього макети. Ваш тест повинен бути максимально наближений до реального застосування. Ви перевіряєте повний випадок використання, а не окрему частину програми.

* тестування блоку black-box vs white-box * Якщо ви використовуєте підхід black-box, чим ви насправді займаєтесь, ви надаєте (усі класи еквівалентності) введення, стан та тести, які отримаєте очікуваний вихід. У такому підході використання макетів взагалі виправдовується (ви просто імітуєте, що вони роблять правильно; ви не хочете їх перевіряти), але викликати Mockito.verify () є зайвим.

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

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

Це справді важко. У мене немає хорошого прикладу, але я можу навести вам приклади. У випадку, про який було сказано вище, рівним () vs equalsIgnoreCase (), ви не повинні викликати Mockito.verify (), просто стверджуйте результат. Якщо ви не змогли цього зробити, розбийте свій код на меншу одиницю, поки не зможете це зробити. З іншого боку, припустимо, у вас є @Service, і ви пишете @ Web-Service, яка по суті є обгорткою для вашої @Service - вона делегує всі дзвінки до @Service (і робить деякі додаткові поводження з помилками). У цьому випадку необхідний дзвінок на Mockito.verify (), ви не повинні дублювати всі ваші чеки, які ви зробили для @Serive, переконуючись, що ви телефонуєте на @Service з правильним списком параметрів.


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

Для мене це найкраща відповідь, оскільки вона відповідає, коли використовувати Mockito.when () у різних ситуаціях. Молодці.
Мічіель Лігвотер

8

Треба сказати, що ви абсолютно праві з точки зору класичного підходу:

  • Якщо ви спершу створите (або зміните) ділову логіку вашої програми, а потім покриєте її (прийняти) тести ( Тест-останній підхід ), тоді буде дуже болісно і небезпечно повідомляти тести про те, як працює ваше програмне забезпечення, крім перевірка входів і виходів.
  • Якщо ви практикуєте тестовий підхід , то ваші тести першими будуть написані, змінені та відображають випадки використання функціональності вашого програмного забезпечення. Реалізація залежить від тестів. Це іноді означає, що ви хочете, щоб ваше програмне забезпечення було реалізоване певним чином, наприклад, покладатися на метод іншого компонента або навіть називати його певну кількість разів. Саме тут Mockito.verify () стане в нагоді!

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


0

Як казали деякі люди

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

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


0

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

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

Через це я вважаю за краще знущатись якнайбільше: також знущаюся над об’єктами даних. Роблячи це, ви можете не тільки перевірити, щоб перевірити, чи називаються правильні методи інших класів, але і чи збираються дані, що передаються, правильними методами цих об'єктів даних. Щоб завершити його, слід перевірити порядок, у якому відбуваються дзвінки. Приклад: якщо ви модифікуєте об'єкт db-сутності, а потім зберігаєте його за допомогою сховища, недостатньо перевірити, чи є виклики заданих об'єктів правильними даними та що викликається метод збереження сховища. Якщо вони викликаються в неправильному порядку, ваш метод все одно не робить те, що повинен робити. Отже, я не використовую Mockito.verify, але я створюю об'єкт InOrder з усіма макетами та використовую натомість inOrder.verify. І якщо ви хочете зробити його завершеним, вам слід також зателефонувати Mockito. в кінці перевіртеNoMoreInteractions і передайте всі макети. Інакше хтось може додати нову функціональність / поведінку, не тестуючи її, що означатиме після, поки ваша статистика покриття може становити 100%, і все ж ви накопичуєте код, який не стверджується чи перевіряється.

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