Це правильне використання методу скидання Mockito?


68

У моєму тестовому класі у мене є приватний метод, який конструює об'єкт, що часто використовується Bar. BarКонструктор викликає someMethod()метод в моєму знущалися об'єкта:

private @Mock Foo mockedObject; // My mocked object
...

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
}

У деяких моїх методах тестування, на які я хочу перевірити, someMethodтакож використовувався саме цей тест. Щось таке:

@Test
public void someTest() {
  Bar bar = getBar();

  // do some things

  verify(mockedObject).someMethod(); // <--- will fail
}

Це не вдається, тому що глузливий об’єкт someMethodвикликав двічі. Я не хочу, щоб мої методи тестування піклувались про побічні ефекти мого getBar()методу, тому було б розумним скинути мій об'єкт макетів наприкінці getBar()?

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
  reset(mockedObject); // <-- is this OK?
}

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

Альтернатива

Альтернативний вибір, схоже, закликає:

verify(mockedObject, times(2)).someMethod();

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

Відповіді:


60

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

  • atLeastOnce() допускає помилкові позитиви, що погано, оскільки ви хочете, щоб ваші тести завжди були правильними.
  • times(2)запобігає хибному позитиву, але робить здається, що ви очікуєте двох викликів, а не сказати "я знаю, що конструктор додає один". Крім того, якщо щось зміниться в конструкторі, щоб додати додатковий виклик, тест тепер має шанс на помилковий позитив. І видалення виклику призведе до того, що тест не вдасться, оскільки тест зараз неправильний, а не те, що тестується, є неправильним.

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

bar = mock(Bar.class);
//do stuff
verify(bar).someMethod();
reset(bar);
//do other stuff
verify(bar).someMethod2();

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


17
Я хотів би, щоб Mockito надав resetInteractions () дзвінок, щоб просто забути минулі взаємодії з метою перевірки (..., разів (...)) і зберегти заглушку. Це зробить тестові ситуації {setup; діяти; verify;} що набагато простіше впоратися. Це було б {встановлення; resetInteractions; діяти; verify}
Аркадій

2
Насправді з Mockito 2.1 він дає спосіб очистити виклики без скидання заглушок:Mockito.clearInvocations(T... mocks)
Колін Д Беннетт

6

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

Замість reset()прохання розглянути питання про написання простих, малих та зосереджених методів тестування на тривалих, завищених тестах. Перший потенційний кодовий запах знаходиться reset()в середині тестового методу.

Витягнуто з док-мотів .

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

Якщо ви насправді не переймаєтесь цим, ви можете скористатися:

verify(mockedObject, atLeastOnce()).someMethod();

Зауважте, що це останнє може спричинити помилковий результат, якщо ви зателефонуєте на якийсь метод з getBar, а не після (це неправильна поведінка, але тест не вийде з ладу).


2
Так, я бачив цю точну цитату (я пов’язав її з моїм запитанням). Наразі я ще не бачу гідного аргументу щодо того, чому мій приклад вище "поганий". Ви можете поставити його?
Дункан Джонс

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

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

Є багато вагомих причин використовувати скидання, я б не приділяв занадто багато уваги цій справі цитата-макето. Ви можете мати Spring Junnit Class Runner Spring під час запуску тестового набору, що викликає небажану взаємодію, особливо якщо ви робите тестування, яке включає знущаються дзвінки до бази даних або дзвінки, що передбачають приватні методи, про які ви не хочете використовувати роздуми.
Сенді Сімонтон

Зазвичай мені здається, що це важко, коли я хочу перевірити кілька речей, але JUnit просто не пропонує жодного приємного (!) Способу параметризації тестів. На відміну від NUnit, наприклад, має примітки.
Стефан Хендрікс

3

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

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

new Bar(mockedObject);

стає:

new Bar(mockedObject.someMethod());

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

public Bar createBar(MockedObject mockedObject) {
    Object dependency = mockedObject.someMethod();
    // ...more logic that used to be in Bar constructor
    return new Bar(dependency);
}

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

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