Это правильное использование метода сброса 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()вспомогательный метод, вы избегаете обеих этих проблем. Тем не менее, вы должны быть осторожны, так как он также сбрасывает все заглушки, которые вы сделали, так что будьте осторожны. Основная причина reset()не рекомендуется, чтобы предотвратить

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

Это не то, что пытается сделать ОП. Я полагаю, что OP имеет тест, который проверяет вызов в конструкторе. Для этого теста сброс позволяет изолировать это единственное действие и его эффект. Этот один из немногих случаев reset()может быть полезен как. Другие варианты, которые не используют все это имеют свои недостатки. Тот факт, что ОП сделал этот пост, показывает, что он думает о ситуации, а не просто слепо использует метод перезагрузки.

unholysampler
источник
17
Я хотел бы, чтобы Mockito предоставил вызов resetInteractions (), чтобы просто забыть прошлые взаимодействия с целью проверки (..., times (...)) и сохранить заглушку. Это сделало бы тестовые ситуации {setup; акт; проверить;} с этим гораздо проще иметь дело. Это было бы {setup; resetInteractions; акт; проверить}
Аркадий
2
На самом деле, начиная с Mockito 2.1, он дает возможность очищать вызовы без сброса заглушек:Mockito.clearInvocations(T... mocks)
Colin D Bennett
6

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

Вместо того, reset()чтобы писать простые, маленькие и сфокусированные методы тестирования, а не длинные, переопределенные тесты. Первый потенциальный запах кода находится reset()в середине метода тестирования.

Извлечено из документов mockito .

Я советую вам избегать использования reset(). По моему мнению, если вы дважды вызываете метод SomeMethod, это следует проверить (возможно, это доступ к базе данных или другой длительный процесс, о котором вы хотите позаботиться).

Если вы действительно не заботитесь об этом, вы можете использовать:

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

Обратите внимание, что это последнее может привести к ложному результату, если вы вызываете someMethod из getBar, а не после (это неправильное поведение, но тест не будет неудачным).

Греза
источник
2
Да, я видел эту точную цитату (я связал ее с моим вопросом). В настоящее время мне еще предстоит увидеть достойный аргумент о том, почему мой приведенный выше пример "плох". Можете ли вы поставить один?
Дункан Джонс
Если вам нужно сбросить фиктивные объекты, похоже, вы пытаетесь проверить слишком много материала в своем тесте. Вы можете разделить его на два теста, тестируя меньшие вещи. В любом случае, я не знаю, почему вы проверяете внутри метода getBar, сложно отследить, что вы проверяете. Я рекомендую вам разработать тестовое мышление в соответствии с тем, что должен делать ваш класс (если вы должны вызывать someMethod ровно два раза, хотя бы один раз, только один раз, никогда и т. Д.), И выполнять каждую проверку в одном и том же месте.
Greuze
Я отредактировал свой вопрос, чтобы подчеркнуть, что проблема сохраняется, даже если я не вызываю verifyсвой частный метод (что, я согласен, вероятно, не относится к этому). Я приветствую ваши комментарии о том, изменится ли ваш ответ.
Дункан Джонс
Есть много веских причин для использования перезагрузки, в этом случае я бы не стал уделять слишком много внимания этой цитате. Вы можете использовать Spring JUnit Class Runner, когда запуск набора тестов вызывает нежелательные взаимодействия, особенно если вы проводите тестирование, которое включает в себя поддельные вызовы базы данных или вызовы, которые включают частные методы, о которых вы не хотите использовать отражение.
Сэнди Симонтон,
Обычно я нахожу это трудным делом, когда я хочу протестировать несколько вещей, но 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 () - хороший способ обойти это. Но давайте будем ясны - это говорит о том, что ваш код плохо спроектирован.

tonicsoft
источник