Когда использовать Mockito.verify ()?

201

Я пишу тестовые примеры jUnit для 3 целей:

  1. Чтобы гарантировать, что мой код удовлетворяет всем требуемым функциям, под всеми (или большинством) входными комбинациями / значениями.
  2. Чтобы убедиться, что я могу изменить реализацию, и положиться на контрольные примеры JUnit, чтобы сказать мне, что все мои функциональные возможности все еще удовлетворены.
  3. В качестве документации по всем сценариям использования мой код обрабатывается и выступает в качестве спецификации для рефакторинга - если когда-либо потребуется переписать код. (Измените код, и если мои тесты jUnit не пройдут - вы, вероятно, пропустили какой-то вариант использования).

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

Я ищу:

  1. Какими должны быть рекомендации по правильному использованию Mockito.verify()?

  2. Правильно ли для jUnits быть в курсе или тесно связано с реализацией тестируемого класса?

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

Ответы:

78

Если контракт класса A включает в себя тот факт, что он вызывает метод B объекта типа C, то вы должны проверить это, сделав макет типа C и проверив, что метод B был вызван.

Это подразумевает, что контракт класса A имеет достаточную детализацию, чтобы говорить о типе C (который может быть интерфейсом или классом). Так что да, мы говорим об уровне спецификации, который выходит за рамки просто «системных требований» и идет каким-то образом к описанию реализации.

Это нормально для юнит-тестов. Когда вы проводите юнит-тестирование, вы хотите убедиться, что каждый юнит выполняет «правильные действия», и это обычно включает его взаимодействие с другими юнитами. «Единицы» здесь могут означать классы или большие подмножества вашего приложения.

Обновить:

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

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

Вот что такое юнит-тесты. Тест, который не страдает от такой зависимости от того, как используются классы коллабораторов, на самом деле является тестом подсистемы или интеграционным тестом. Конечно, они также часто пишутся с помощью JUnit и часто включают использование насмешек. На мой взгляд, «JUnit» - ужасное название для продукта, который позволяет нам производить все виды тестов.

Дауд ибн Карим
источник
8
Спасибо, Дэвид. После сканирования некоторых наборов кодов это кажется обычной практикой, но для меня это сводит на нет цель создания модульных тестов и просто добавляет издержки на их обслуживание за очень небольшую ценность. Я понимаю, почему требуются макеты, и почему необходимо установить зависимости для выполнения теста. Но проверка того, что метод dependencyA.XYZ () выполняется, на мой взгляд, делает тесты очень хрупкими.
Рассел
@Russell Даже если «тип C» является интерфейсом для оболочки вокруг библиотеки или вокруг какой-то отдельной подсистемы вашего приложения?
Дауд ибн Карим
1
Я бы не сказал, что совершенно бесполезно гарантировать, что какая-то подсистема или служба была вызвана - просто что должны быть некоторые руководящие принципы (я хотел бы сформулировать их). Например: (я, вероятно, слишком упрощаю это) Скажем, я использую StrUtil.equals () в своем коде и решаю переключиться на StrUtil.equalsIgnoreCase () в реализации. Если jUnit должен был проверить (StrUtil.equals) ), мой тест может не пройти, хотя реализация точна. Этот проверочный вызов, IMO, является плохой практикой, хотя он предназначен для библиотек / подсистем. С другой стороны, использование verify для гарантии того, что вызов closeDbConn может быть допустимым вариантом использования.
Рассел
1
Я вас понимаю и полностью с вами согласен. Но я также чувствую, что написание руководящих принципов, которые вы описываете, может привести к написанию всего учебника по TDD или BDD. Возьмем, к примеру, вызов equals()или equalsIgnoreCase()никогда не будет что-то, что было указано в требованиях класса, так что никогда не было бы модульного теста как такового. Однако «закрытие соединения с БД по окончании» (что бы это ни значило с точки зрения реализации) вполне может быть требованием класса, даже если это не «бизнес-требование». Для меня это сводится к отношениям между контрактом ...
Дауд ибн Карим
... класса в соответствии с его бизнес-требованиями и набором методов тестирования, которые проводят модульное тестирование этого класса. Определение этих отношений было бы важной темой в любой книге по TDD или BDD. В то время как кто-то из команды Mockito мог написать пост на эту тему для своей вики, я не понимаю, чем он будет отличаться от множества других доступных публикаций. Если вы видите, как это может отличаться, дайте мне знать, и, возможно, мы сможем поработать над этим вместе.
Дауд ибн Карим
60

Ответ Дэвида, конечно, правильный, но не совсем объясняет, почему вы этого хотите.

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

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

Или вы могли бы посмеяться над DAO и убедиться, что он вызывается так, как вы ожидаете. С помощью mockito вы можете убедиться, что что-то вызывается, как часто оно вызывается, и даже использовать сопоставления параметров, чтобы убедиться, что оно вызывается определенным образом.

Обратная сторона модульного тестирования, подобного этому, заключается в том, что вы привязываете тесты к реализации, что делает рефакторинг немного сложнее. С другой стороны, хороший дизайнерский запах - это количество кода, необходимого для его правильного использования. Если ваши тесты должны быть очень длинными, возможно, что-то не так с дизайном. Так что код с большим количеством побочных эффектов / сложных взаимодействий, которые необходимо протестировать, вероятно, не очень хорошая вещь.

Жиль ван Гурп
источник
30

Это отличный вопрос! Я думаю, что основная причина этого заключается в следующем, мы используем JUnit не только для модульного тестирования. Таким образом, вопрос должен быть разделен:

  • Должен ли я использовать Mockito.verify () в моей интеграции (или любом другом тестировании выше единицы)?
  • Должен ли я использовать Mockito.verify () в моем черном ящике модульном тестировании ?
  • Должен ли я использовать Mockito.verify () в моем модульном тестировании белого ящика ?

так что, если мы будем игнорировать тестирование выше, чем модуль, вопрос можно перефразировать: « Использование модульного тестирования белого ящика с Mockito.verify () создает отличную пару между модульным тестом и моей возможной реализацией, могу ли я создать некоторый « серый ящик » « юнит-тестирование и какие практические правила я должен использовать для этого ».

Теперь давайте пройдемся по всему этому шаг за шагом.

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

* черный ящик против модульного тестирования белого ящика * Если вы используете подход черного ящика, что вы действительно делаете, вы предоставляете (все классы эквивалентности) входные данные, состояние и тесты, которые вы получите ожидаемый результат. При таком подходе использование mocks в целом оправдывает (вы просто имитируете, что они делают правильные вещи; вы не хотите их проверять), но вызов Mockito.verify () излишен.

Если вы используете подход « белого ящика», что вы действительно делаете, вы тестируете поведение своего устройства. В этом подходе необходим вызов Mockito.verify (), вы должны убедиться, что ваш юнит ведет себя так, как вы ожидаете.

правила большого пальца для тестирования « серого ящика» Проблема с тестированием белого ящика заключается в том, что оно создает высокую связь. Одним из возможных решений является тестирование «серого ящика», а не «белого ящика». Это своего рода комбинация черно-белого тестирования. Вы действительно тестируете поведение вашего модуля, как в тесте белого ящика, но в целом вы делаете его независимым от реализации, когда это возможно . Когда это возможно, вы просто сделаете проверку, как в случае с черным ящиком, просто подтвердите, что выход - это то, что вы ожидаете. Итак, суть вашего вопроса в том, когда это возможно.

Это действительно сложно. У меня нет хорошего примера, но я могу привести примеры. В случае, который был упомянут выше с помощью equals () vs equalsIgnoreCase (), вы не должны вызывать Mockito.verify (), просто подтвердить вывод. Если вы не смогли этого сделать, разбейте ваш код на меньшие единицы, пока не сможете это сделать. С другой стороны, предположим, что у вас есть некоторый @Service и вы пишете @ Web-Service, который по сути является оберткой для вашего @Service - он делегирует все вызовы @Service (и делает некоторую дополнительную обработку ошибок). В этом случае необходим вызов Mockito.verify (), вам не следует дублировать все свои проверки, которые вы делали для @Serive, достаточно убедиться, что вы вызываете @Service с правильным списком параметров.

alexsmail
источник
Тестирование серая коробка - немного ловушка. Я склонен ограничивать это такими вещами, как DAO. Я участвовал в некоторых проектах с очень медленной сборкой из-за обилия тестов «серого ящика», почти полного отсутствия модульных тестов и слишком большого количества тестов «черного ящика», чтобы компенсировать недостаток доверия к тому, что якобы тестировали тесты greybox.
Жилль ван Гурп
Для меня это лучший доступный ответ, так как он отвечает, когда использовать Mockito.when () в различных ситуациях. Отлично сработано.
Michiel Leegwater
8

Должен сказать, что вы абсолютно правы с точки зрения классического подхода:

  • Если вы сначала создаете (или изменяете) бизнес-логику своего приложения, а затем покрываете ее (принимаете) тесты ( подход Test-Last ), тогда будет очень болезненно и опасно сообщать тестам что-либо о том, как работает ваше программное обеспечение, кроме проверка входов и выходов.
  • Если вы практикуете подход , основанный на тестировании , то ваши тесты первыми будут написаны, изменены и отражают варианты использования функциональности вашего программного обеспечения. Реализация зависит от тестов. Иногда это означает, что вы хотите, чтобы ваше программное обеспечение было реализовано определенным образом, например, полагалось на метод какого-либо другого компонента или даже вызывало его определенное количество раз. Вот где Mockito.verify () пригодится!

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

hammelion
источник
0

Как говорили некоторые люди

  1. Иногда у вас нет прямого выхода, на котором вы можете утверждать
  2. Иногда вам просто нужно подтвердить, что ваш проверенный метод отправляет правильные косвенные результаты своим соавторам (над которыми вы издеваетесь).

Что касается вашего беспокойства по поводу нарушения ваших тестов при рефакторинге, то это вполне ожидаемо при использовании mocks / stubs / spies. Я имею в виду, что по определению, а не в отношении конкретной реализации, такой как Mockito. Но вы могли бы подумать так: если вам нужно провести рефакторинг, который приведет к серьезным изменениям в том, как работает ваш метод, это хорошая идея сделать это с подходом TDD, то есть вы можете сначала изменить свой тест, чтобы определить новое поведение (которое не пройдёт тест), а затем внесите изменения и получите тест снова пройденным.

Эмануэль Луис Ларигет Бельтрам
источник
0

В большинстве случаев, когда людям не нравится использовать Mockito.verify, это потому, что он используется для проверки всего, что делает тестируемый модуль, и это означает, что вам нужно будет адаптировать свой тест, если что-то изменится в нем. Но я не думаю, что это проблема. Если вы хотите иметь возможность изменять то, что делает метод без необходимости менять его тест, это в основном означает, что вы хотите писать тесты, которые не проверяют все, что делает ваш метод, потому что вы не хотите, чтобы он тестировал ваши изменения , И это неправильный способ мышления.

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

Из-за этого я предпочитаю издеваться как можно больше: также издеваться над объектами данных. При этом вы можете не только использовать verify, чтобы проверить, что вызываются правильные методы других классов, но также и то, что передаваемые данные собираются с помощью правильных методов этих объектов данных. И чтобы сделать это, вы должны проверить порядок, в котором происходят вызовы. Пример: если вы изменяете объект сущности db, а затем сохраняете его с помощью репозитория, недостаточно проверить, что установщики объекта вызываются с правильными данными и что вызывается метод сохранения репозитория. Если они вызываются в неправильном порядке, ваш метод все равно не выполняет то, что должен делать. Итак, я не использую Mockito.verify, но создаю объект inOrder со всеми имитаторами и вместо этого использую inOrder.verify. И если вы хотите завершить его, вам также следует позвонить в Mockito. verifyNoMoreInteractions в конце и передать все это макеты. В противном случае кто-то может добавить новую функциональность / поведение без его тестирования, что может означать, что через некоторое время ваша статистика покрытия может быть 100%, и все же вы накапливаете код, который не утвержден или не проверен.

Стефан Монделерс
источник