О статье
Перед вами очередное руководство по Mockito. В нём я, с одной стороны, попытался описать функционал этой библиотеки так, чтобы незнакомый с нею читатель сразу получил возможность полноценно ею пользоваться, а не только общее представление о ней. С другой — я хотел сделать его достаточно компактным и структурированным, чтобы можно было быстро прочесть его целиком и быстро найти в нём что-то единожды прочитанное, но подзабытое. В общем, эта такая статья, какая бы пригодилась мне самому, когда я только столкнулся с этой библиотекой и не очень понимал, как она работает.
Полагаю, она и сейчас может пригодиться мне же — иногда я забываю что-то из этого, а вспоминать материал удобнее всего не по официальной документации или чужим статьям, а по собственному, скажем так, конспекту. Вместе с тем я старался построить текст так, чтобы он был удобен прежде всего для знакомства с Mockito с нуля, и кое-где подробно разбираю вроде бы очевидные вещи — не все из которых были для меня очевидными с самого начала.
Содержание:
- Mockito: что это такое и зачем нужно
- Окружение, версии и подопытное животное
- mock и spy
- Управление поведением
- Слежение за вызовами методов
- Mock-объекты как значения полей и аннотации Mockito
- Откат поведения к дефолтному и сессии Mockito
- Что ещё?
Mockito: что это такое и зачем нужно
Говоря коротко, Mockito — фреймворк для работы с заглушками.
Как известно, при тестировании кода (прежде всего юнит-тестировании, но не только) тестируемому элементу часто требуется предоставить экземпляры классов, которыми он должен пользоваться при работе. При этом часто они не должны быть полнофункциональными — наоборот, от них требуется вести себя жёстко заданным образом, так, чтобы их поведение было простым и полностью предсказуемым. Они и называются заглушками (stub). Чтобы их получить, можно создавать альтернативные тестовые реализации интерфейсов, наследовать нужные классы с переопределением функционала и так далее, но всё это достаточно неудобно, избыточно и чревато ошибками. Более удобное во всех смыслах решение — специализированные фреймворки для создания заглушек. Одним из таковых (и, пожалуй, самым известным для Java) и является Mockito.
Mockito позволяет создать одной строчкой кода так называемый mock (что-то вроде основы для нужной заглушки) любого класса. Для такого mock сразу после создания характерно некое поведение по умолчанию (все методы возвращают заранее известные значения — обычно это null
либо 0
). Можно переопределить это поведение желаемым образом, проконтролировать с нужной степенью детальности обращения к ним так далее. В результате mock и становится заглушкой с требуемыми свойствами. Ниже я подробно разберу, как это сделать.
Отмечу, что mock можно создать и для тех классов, новый экземпляр которых вообще-то так просто не создашь, в частности, классов с исключительно приватными конструкторами типа синглтонов и утилитных классов, а при минимальной настройке фреймворка — и перечислений (enums).
Окружение, версии и подопытное животное
При написании этой статьи я использовал:
- Mockito: 'org.mockito:mockito-core:2.24.0' (последняя стабильная версия на момент написания)
- TestNG: 'org.testng:testng:6.14.3' в качестве тестового фреймворка
- AssertJ: 'org.assertj:assertj-core:3.11.1' в качестве инструмента проверок
- Lombok: 'org.projectlombok:lombok:1.18.6' (просто для удобства)
- Java 8
Для своих бесчеловечных экспериментов я написал вот такой интерфейс сервиса, предоставляющего доступ к неким данным.
public interface DataService {
void saveData(List<String> dataToSave);
String getDataById(String id);
String getDataById(String id, Supplier<String> calculateIfAbsent);
List<String> getData();
List<String> getDataListByIds(List<String> idList);
List<String> getDataByRequest(DataSearchRequest request);
}
А это (пусть уж будет для порядка) код класса запроса, передаваемого в последний из методов интерфейса.
@AllArgsConstructor
@Getter
class DataSearchRequest {
String id;
Date updatedBefore;
int length;
}
Единицы данных идентифицируются по ID и имеют ещё некоторые характеристики, но непосредственно в том виде, в котором возвращаются сервисом, они представляют собой строки, а не какие-то более сложные объекты. Ничего важного я так не упускаю, а примеры получаются проще и нагляднее.
Сразу же отмечу: в примерах ниже я для наглядности непосредственно вызываю переопределённые методы моих mock-объектов, но при реальном тестировании идея вовсе не в этом! В настоящем тесте я бы последовательно выполнил следующее:
- настроил mock моего сервиса нужным образом;
- передал его (вероятнее всего, через конструктор) экземпляру использующего его другого класса (предположим, содержащего какую-то бизнес-логику, использующую предоставляемые
DataService
данные), который я, собственно, и тестировал бы; - задействовал функционал тестируемого класса и проконтролировал бы результаты;
- при необходимости проконтролировал бы количество и порядок вызовов метода(ов) моего mock, которые должны были быть вызваны тестируемым классом в результате предыдущего действия.
mock и spy
Центральный класс Mockito, через который предполагается обращаться к большей части функционала, — это, собственно, класс под названием Mockito
(есть также класс BDDMockito
, предоставляющий примерно те же возможности в форме, более подходящей для BDD, но здесь я не стану на нём останавливаться). Доступ к функционалу реализован через его статические методы.
Чтобы создать mock класса DataService
, я должен сделать всего лишь следующее:
DataService dataServiceMock = Mockito.mock(DataService.class);
Готово — я получил экземпляр нужного мне класса. Он будет принят любым методом или конструктором, которому требуется параметр такого типа (например, конструктором того класса, который я хочу протестировать). Даже если далее его ожидает проверка с пристрастием, он её пройдёт: не только instanceof DataService
вернёт true
, но и dataServiceMock.getClass()
— именно DataService.class
. Каким-то формальным образом программно отличить mock-объект от обычного оказывается довольно непростой задачей, что и логично: ведь первый предназначен как раз для того, чтобы быть неотличимым от второго. Однако в составе Mockito для этого есть инструмент — метод Mockito.mockingDetails
. Передав ему произвольный объект, я получу объект класса MockingDetails
. Он содержит информацию о том, что этот объект представляет собой с точки зрения Mockito: является ли он mock, spy (см. ниже), как использовался, как был создан и прочее.
Особо нужно упомянуть ситуацию, когда я пытаюсь создать mock для final класса или mock-экземпляр enum либо переопределить поведение final метода. В таком случае при поведении Mockito по умолчанию код выше откажется работать, сославшись именно на это обстоятельство. Однако это можно изменить — достаточно создать в проекте (при стандартном устройстве проектного дерева каталогов) файл test/resources/mockito-extensions/org.mockito.plugins.MockMaker
и вписать в него строчку:
mock-maker-inline
После этого можно имитировать обычным способом final классы и enum'ы, а также переопределять final методы.
Полученный мной mock в действии максимально безлик: ни один метод при вызове не окажет никакого воздействия на что бы то ни было, а возвращённое значение окажется null
для объектных типов и 0
для примитивных. Обратите внимание: если метод возвращает коллекцию, mock'ом по умолчанию будут возвращены не null
'ы, а пустые экземпляры коллекций. Например, для List
это окажется пустой LinkedList
независимо от того, что должен был возвращать реальный метод. А вот в качестве значений массивов, примитивных или объектных, я получу null
. Поведение по умолчанию (и не только его) можно изменить при помощи функционала класса MockSettings
, но это нечасто бывает нужно.
Так или иначе, в большинстве случаев мне понадобится не поведение по умолчанию, и в следующем разделе я подробно разберу, как задавать вместо него то, которое требуется.
Однако что, если я хочу использовать в качестве заглушки объект реального класса с имеющимся функционалом, переопределив работу только части его методов? Если речь о юнит-тестировании, такая потребность обычно (но не всегда) свидетельствует о том, что в проекте не всё в порядке с дизайном, и в принципе так действовать не рекомендуется. Однако случаются ситуации, когда этого по какой-то причине не избежать. На этот случай в Mockito есть так называемые spy, "шпионы". В отличие от mock'ов, их можно создавать на основе как класса, так и готового объекта:
DataService dataServiceSpy = Mockito.spy(DataService.class);
// or
DataService dataService = new DataService();
dataServiceSpy = Mockito.spy(dataService);
При создании spy на основе класса, если его тип — интерфейс, будет создан обычный mock-объект, а если тип — класс, то Mockito попытается создать экземпляр при помощи конструктора по умолчанию (без параметров). И только если такого конструктора нет, произойдёт ошибка и тест не сработает.
Поведение spy-объектов по умолчанию идентично поведению обычного экземпляра класса, однако они дают мне те же возможности, что и mock-объекты: позволяют переопределять их поведение и наблюдать за их использованием (см. следующие разделы). Важный момент: spy — не обёртка вокруг того экземпляра, на основе которого он создан! Поэтому вызов метода spy на состояние изначального экземпляра не повлияет.
Управление поведением
Итак, о том, как заставить mock или spy делать то, что мне нужно. Далее я буду везде писать просто "mock" — это будет значить "mock или spy", кроме случаев, где прямо сказано иное.
В целом управление поведением mock-объекта сводится к одной очевидной концепции: когда на mock так-то воздействовали (то есть вызван такой-то метод с такими-то аргументами), он должен отреагировать так-то и так-то. У этой концепции существуют две реализации в рамках класса Mockito — основная, рекомендуемая разработчиками к использованию везде, где это возможно, и альтернативная, применяемая там, где основная не годится.
Основная реализация базируется на методе Mockito.when
. Этот метод принимает в качестве "параметра" вызов переопределяемого метода mock-объекта (таким образом фиксируется определяемое воздействие) и возвращает объект типа OngoingStubbing
, позволяющий вызвать один из методов семейства .then...
(так задаётся реакция на это воздействие). Всё вместе в простейшем случае выглядит примерно так:
List<String> data = new ArrayList<>();
data.add("dataItem");
Mockito.when(dataService.getAllData()).thenReturn(data);
После этой операции я, вызвав у объекта dataService
метод getAllData()
, получу объект, заданный в первой строчке листинга.
Здесь привычная "объектно-ориентированная" интуиция может дать некоторый сбой, так что на этом стоит остановиться чуть подробнее. С точки зрения синтаксиса Java значением, передаваемым методу when
в качестве параметра, является, разумеется, значение, возвращаемое переопределяемым методом. Для mock это пустое значение, для spy — значение, возвращаемое методом реального объекта. Но благодаря действующей "под капотом" Mockito магии метод when
сработает штатным образом (а не упадёт при запуске с ошибкой) лишь в том случае, если внутри скобок после when
находится именно вызов метода mock-объекта.
Подобная идеология часто действует при задании поведения mock в Mockito: вызывая метод (mock-объекта или класса Mockito
), я стремлюсь не получить возвращаемое им значение, а каким-то образом повлиять на возможный вызов того метода mock-объекта, с которым работаю: указать его границы, задать результат, установить наблюдение за его вызовами и так далее. Звучит несколько туманно, признаю, и при первом столкновении выглядит странно, но, разобравшись, очень скоро начинаешь ощущать этот подход как совершенно естественный в контексте работы с заглушками.
Альтернативная реализация связывания условия и результата вызова — методы семейства Mockito.do...
. Эти методы позволяют задать поведение начиная с результата вызова и возвращают объект класса Stubber
, уже при помощи которого можно задать условие. То же самое связывание, что и выше, выполненное этим способом, выглядит так:
List<String> data = new ArrayList<>();
data.add("dataItem");
Mockito.doReturn(data).when(dataService).getData()
В чём разница, почему связывание через Mockito.when
считается предпочтительным и когда всё-таки приходится использовать методы Mockito.do...
? Обратите внимание: в первой реализации при задании поведения метода (в данном случае getAllData()
) сначала выполняется вызов ещё не переопределённой его версии, и только потом, в недрах Mockito, происходит переопределение. Во второй же такого вызова не происходит — методу Stubber.when
передаётся непосредственно mock, а уже у возвращённого этим методом объекта того же типа, но другой природы совершается вызов переопределяемого метода. Эта разница всё и определяет. Связывание через Mockito.do...
никак не контролирует на стадии компиляции то, какой переопределяемый метод я вызову и совместим ли он по типу с заданным возвращаемым значением. Поэтому обычно Mockito.when
предпочтительнее — там с этим ошибки быть не может. Зато возможны случаи, когда я хочу избежать вызова непереопределённого метода — для свежесозданного mock такой вызов вполне приемлем, но если я ранее уже переопределил этот метод или имею дело со spy, он может оказаться нежелательным, а при выбрасывании исключения и вовсе не позволит выполнить нужное переопределение. И вот тут на помощь приходит связывание через Mockito.do...
.
Ещё одна ситуация, где не обойтись без методов Mockito.do...
, — переопределение метода, возвращающего void
: ожидающий параметра Mockito.when
с таким методом работать не может. Mockito.doReturn
тут, понятно, не у дел, зато есть Mockito.doThrow
, Mockito.doAnswer
и достаточно редко пригождающийся Mockito.doNothing
.
Далее я рассмотрю чуть подробнее способы задания условий и результатов вызовов. Я буду рассматривать только связывание через Mockito.when
— альтернативный способ практически полностью аналогичен в обращении.
Задание условий вызова
Пример выше касается метода без параметров, и связанное с ним условие вызова возможно одно — сам факт вызова. Как только появляются параметры, ситуация становится сложнее. Как минимум, для вызова метода, поведение которого я задаю, мне нужно что-то ему передать. Но важнее другое: может оказаться, что задаваемую реакцию я хочу получать не всегда, а только при вызове с параметрами, отвечающими определённым требованиям. DataService
имеет вот такой метод:
String getDataItemById(String id) {
// some code...
}
Если мне нужно задать реакцию на любой вызов этого метода независимо от аргументов, я должен воспользоваться методом Mockito.any
:
Mockito.when(dataService.getDataItemById(any()))
.thenReturn("dataItem");
Если же мне требуется, чтобы mock реагировал только на определённое значение аргумента, можно использовать непосредственно это значение или методы Mockito.eq
(когда речь об эквивалентности) либо Mockito.same
(когда требуется сравнение ссылок):
Mockito.when(dataService.getDataItemById("idValue"))
.thenReturn("dataItem");
// or
Mockito.when(dataService.getDataItemById(Mockito.eq("idValue")))
.thenReturn("dataItem");
А если я хочу, чтобы аргумент отвечал каким-то требованиям, для этого есть ряд удобных специализированных статических методов того же класса Mockito
(например, строки можно проверить на содержание в начале или в конце определённой последовательности символов, соответствие паттерну и др.). Также имеется общий метод Mockito.argThat (и его аналоги для примитивных типов), принимающий реализацию функционального интерфейса ArgumentMatcher:
Mockito.when(dataService.getDataById(
Mockito.argThat(arg -> arg == null || arg.length() > 5)))
.thenReturn("dataItem");
Классы ArgumentMatchers
и AdditionalMatchers
позволяют работать с некоторыми полезными готовыми реализациями этого интерфейса. Например, AdditionalMatchers.or
и AdditionalMatchers.and
позволяют комбинировать другие матчеры (обратите внимание: статические методы этих классов не возвращают экземпляры матчеров, а только обращаются к ним!)
Для одного и того же метода можно задать поведение несколько раз с разными требованиями к аргументам, и все определённые таким образом модели поведения будут действовать одновременно. Разумеется, в каких-то случаях они могут пересекаться — скажем, я потребую вернуть один результат при получении значения int
параметра меньше 5 и другой — при получении чётного значения. В такой ситуации приоритет имеет то поведение, которое задано позже. Поэтому при задании сложных схем поведения следует начинать с самых слабых требований (в пределе — any()
) и уже затем переходить к более специфическим.
При работе с методами с более чем одним аргументом заданные требования комбинируются в соответствии с логическим И, то есть для получения заданного результата КАЖДЫЙ из аргументов должен отвечать поставленному требованию. Я не нашёл способа скомбинировать требования произвольным образом, хотя, возможно, он существует.
Кроме того, при задании поведения такого метода нельзя комбинировать использующие матчеры статические методы Mockito
и прямую передачу значений. Используйте Mockito.eq
или Mockito.same
.
Задание результатов вызова
После того, как метод mock-объекта вызван, объект должен отреагировать на вызов. Основные возможные последствия — возвращение результата и выбрасывание исключения, и именно на эти варианты в первую очередь рассчитан инструментарий Mockito.
В простейшем случае, уже показанном выше, реакция на вызов — возвращение значения. Приведу его код ещё раз:
List<String> data = new ArrayList<>();
data.add("dataItem");
Mockito.when(dataService.getAllData()).thenReturn(data);
Обратите внимание: вернуть можно только объект, отдельных методов для примитивов не предусмотрено. Поэтому, если метод возвращает примитивное значение, в такой ситуации будет происходит un/boxing. В большинстве случаев это никак не мешает, но если компилятор считает иначе, придётся как-то с ним договариваться… или смириться с его предупреждениями.
Бросить исключения ничуть не сложнее:
Mockito.when(dataService.getDataById("invalidId"))
.thenThrow(new IllegalArgumentException());
Есть и другой способ: можно создать объект исключения и бросить непосредственно его, а можно предоставить Mockito только класс исключения, чтобы оно было создано автоматически:
Mockito.when(dataService.getDataById("invalidId"))
.thenThrow(IllegalArgumentException.class);
В обоих случаях синтаксис позволяет использовать и checked исключения, однако Mockito не позволит запустить такой тест, если тип исключения не соответствует методу, который я хочу заставить бросить это исключение.
При использовании класса как параметра конструкторы (даже без параметров), а равно и прямая инициализация полей, игнорируются — объект создаётся в обход них (в конце концов, это же Mockito!), так что все поля брошенного исключения будут равны null
. Поэтому, если для вас имеет значение содержимое исключения (допустим, какое-нибудь поле type
, у которого есть значение по умолчанию), придётся отказаться от этого способа и создавать исключения вручную.
Эти варианты реакции подходят, если в ответ на вызов с заданными условиями нужно всегда возвращать определённое, всегда одно и то же значение результата или выбрасывать всегда одинаковое исключение, и в большинстве случаев этих возможностей вполне достаточно. Но как быть, если требуется бо́льшая гибкость? Предположим, мой метод принимает коллекцию значений, а возвращает другую коллекцию значений, связанных с первыми одно к одному (например, это получение коллекции объектов данных по набору их ID), и я хочу в рамках теста использовать этот mock-объект неоднократно с разными наборами входных данных, получая каждый раз соответствующий результат. Можно, конечно, описать по отдельности реакцию на каждый конкретный набор параметров, но есть более удобное решение — метод .thenAnswer
, он же .then
. Он принимает реализацию функционального интерфейса Answer
, единственный метод которого получает объект класса InvocationOnMock
. У последнего я могу запросить параметры вызова метода (один по номеру или все сразу в виде массива) и поступить с ними, как мне заблагорассудится. Например, можно получить для каждого из элементов моей коллекции соответствующее ему значение, сформировать из них новую коллекцию и вернуть её (обратите внимание: желаемый результат просто возвращается, а не записывается в какое-то поле объекта-параметра, как можно было бы ожидать):
Mockito.when(dataService.getDataByIds(Mockito.any()))
.thenAnswer(invocation -> invocation
.<List<String>>getArgument(0).stream()
.map(id -> {
switch (id) {
case "a":
return "dataItemA";
case "b":
return "dataItemB";
default:
return null;
}
})
.collect(Collectors.toList()));
Идеологически это что-то вроде написания модели реального метода: получение параметров, обработка, возвращение результата. В принципе ничто не мешает мне реализовать здесь и какую-то другую логику, дополнительно влияющую на что-нибудь ещё, но если такая необходимость возникает регулярно, это может указывать на то, что я заставляю mock-объекты выполнять несвойственные им задачи.
Есть также готовые реализации Answer
, предоставляющие наиболее типичные варианты функционала, — например, AnswersWithDelay
, ReturnsElementsOf
и т. д.
Обратите внимание: типобезопасности InvocationOnMock
не обеспечивает — аргументы возвращаются либо в виде массива Object[]
, либо generic-методом.
Отдельно стоит упомянуть ещё один вариант реакции — thenCallRealMethod
. Предназначение понятно из названия. Он действует как для mock-, так и для spy-объектов. В случае mock все поля объекта, к которым может обратиться код метода, будут опять-таки иметь значение null
. Для spy же использование thenCallRealMethod
означает возвращение к поведению spy по умолчанию; это может быть пригодится, если я переопределил поведение какого-то метода и теперь хочу вернуть прежнее.
Такой вариант реакции также можно получить через thenAnswer
: объект InvocationOnMock
имеет метод callRealMethod()
— это может пригодиться, если нужно "обернуть" вызов реального кода какой-то дополнительной логикой.
Все перечисленные методы OngoingStubbing
возвращают также объект OngoingStubbing
, у которого, в свою очередь, можно вызвать любой из них. Таким образом задаётся последовательность разных реакций на один и тот же вызов, совершённый несколько раз. Методы thenReturn
и thenThrow
имеют перегруженные версии, принимающие varargs. Они позволяют сделать то же самое компактнее.
Mockito.when(dataService.getDataById("a"))
.thenReturn("valueA1", "valueA2")
.thenThrow(IllegalArgumentException.class);
Здесь первый вызов метода с заданным параметром вернёт "valueA1
, второй — "valueA2
(не спрашивайте), а третий (как и все последующие) будет вызывать выбрасывание IllegalArgumentException
.
Слежение за вызовами методов
Всё вышеописанное служит главным образом решению одной задачи: удовлетворить потребность тестируемого нами класса в объектах других классов (mock'и которых мы и создаём), обладающих нужным нам предсказуемым поведением. Может возникнуть другая, в некотором смысле обратная задача: убедиться в том, что тестируемый класс вызывает методы этих объектов нужное число раз, в нужном порядке и с нужными параметрами. Для этого предназначены методы семейства Mockito.verify...
.
Простейший вариант, когда я проверяю факт однократного вызова метода на протяжении выполнения теста, выглядит так:
Mockito.verify(dataService).getDataById(Mockito.any());
Тест с такой конструкцией пройдёт успешно, если она находится после единственного за время выполнения теста вызова метода getDataById
, и упадёт, если метод не был вызван или был вызван дважды и более. Заметьте, что на количество вызовов никак не влияет ни одно служебное выражение самого Mockito, включая задание поведения с помощью when
, хотя в нём, казалось бы, и присутствует непосредственно вызов нужного метода у нужного mock-объекта. Это, однако, не значит, что с каждым новым вызовом when
счётчик сбрасывается, — подсчёт идёт от самого создания mock'а, если только его не обнуляли специально (см. ниже).
То же самое можно выразить при помощи более гибкого перегруженного метода:
Mockito.verify(dataService, Mockito.times(1))
.getDataById(Mockito.any());
Здесь я могу указать ожидаемое количество вызовов при помощи метода Mockito.times
; для отсутствия вызовов существует также шоткат Mockito.never
. Ещё здесь применимы Mockito.atLeast
(с шоткатом Mockito.atLeastOnce
для значения 1) и Mockito.atMost
, устанавливающие соответственно минимальное и максимальное количество вызовов, и странный метод Mockito.only
, проверяющий, что вызов метода был единственным обращением к данному mock-объекту вообще (т. е. другие методы вызваны не были).
Кроме того, одноимённые методы могут быть вызваны не непосредственно у Mockito
, а у объектов классов VerificationAfterDelay
и VerificationWithTimeout
, возвращаемых соответственно методами Mockito.after
и Mockito.timeout
. Например:
Mockito.verify(dataService, Mockito.after(1000).times(1))
.getDataById(Mockito.any());
В этом случае проверка, не обнаружившая у mock нужного числа вызовов, не приводит сразу к падению теста, а сперва ждёт в течение заданного в миллисекундах периода времени в расчёте на то, что эти вызовы всё же будут совершены. Это возможность полезна при работе с многопоточным кодом. Между собой after
и timeout
различаются тем, что в первом случае проверка успешно проходит только после того, как заданный период завершится, а во втором — сразу после того, как требуемое условие окажется выполненным. Таким образом, при использовании timeout
вызовов соответствующего метода может оказаться и больше требуемого — на успешность прохождения теста это не повлияет. Поэтому у VerificationWithTimeout
нет методов never
и atMost
: с учётом принципа его работы в них нет смысла.
Как видите, в качестве параметра для вызываемого метода я использую здесь уже встречавшийся выше Mockito.any()
. На его месте могут быть и другие обращения к матчерам, перечисленные там же, или непосредственно значения параметров — в этих случаях Mockito проверит количество вызовов заданного метода не вообще, а именно с параметрами, соответствующими заданным таким образом требованиям. Mock-объект сохраняет информацию об истории вызовов, и ничто не мешает подвергнуть её нескольким проверкам, если это нужно, например, так:
dataService.getDataById("a");
dataService.getDataById("b");
Mockito.verify(dataService, Mockito.times(2)).getDataById(Mockito.any());
Mockito.verify(dataService, Mockito.times(1)).getDataById("a");
Mockito.verify(dataService, Mockito.never()).getDataById("c");
dataService.getDataById("c");
Mockito.verify(dataService, Mockito.times(1)).getDataById("c");
Mockito.verifyNoMoreInteractions(dataService);
В конце я вызываю метод verifyNoMoreInteractions
(он же verifyZeroInteractions
) — он проверяет отсутствие каких-либо неверифицированных (то есть не подпадающих ни под один из выполненных до этого вызовов verify
) обращений к моему mock-объекту — к любым его методам. Обратите внимание: метод принимает varargs, но это совсем не означает, как можно было бы подумать, что речь о проверке взаимодействия переданных ему объектов между собой!
Код выше проверяет факт определённого количества вызовов, но не то, в каком порядке они были совершены, а это тоже может понадобиться. Чтобы контролировать порядок, нужно получить объект InOrder
:
InOrder inOrder = Mockito.inOrder(dataService);
Этот метод тоже принимает varargs; порядок добавления не важен — несколько переданных ему mock-объектов означают всего лишь, что полученный объект InOrder
можно будет использовать для контроля за порядком вызова методов всех этих объектов относительно друг друга. Сам метод имеет методы verify
с теми же сигнатурами, что и Mockito.verify
:
inOrder.verify(dataService, times(2)).saveData(any());
inOrder.verify(dataService).getData();
Такой тест пройдёт только в том случае, если до приведённого фрагмента был дважды вызван метод saveData
, а потом единожды — getData
. Обратите внимание, что объект InOrder
можно сгенерировать и до, и после подлежащих учёту вызовов — он в любом случае сработает.
Чтобы проконтролировать наличие вызова с определённым параметром, вполне достаточно матчеров, когда речь идёт о простых параметрах — строках, например. Если же речь об экземпляре какого-то более сложного класса со множеством полей, значения которых нужно проверить, может быть удобнее поступить иначе — перехватить параметр, с которым метод будет вызван, и проанализировать его отдельно. С этим поможет класс ArgumentCaptor
и его метод capture()
. Например:
DataSearchRequest request = new DataSearchRequest("idValue", new Date(System.currentTimeMillis()), 50);
dataService.getDataByRequest(request);
ArgumentCaptor<DataSearchRequest> requestCaptor = ArgumentCaptor.forClass(DataSearchRequest.class);
Mockito.verify(dataService, times(1)).getDataByRequest(requestCaptor.capture());
assertThat(requestCaptor.getAllValues()).hasSize(1);
DataSearchRequest capturedArgument = requestCaptor.getValue();
assertThat(capturedArgument.getId()).isNotNull();
assertThat(capturedArgument.getId()).isEqualTo("idValue");
assertThat(capturedArgument.getUpdatedBefore()).isAfterYear(1970);
assertThat(capturedArgument.getLength()).isBetween(0, 100);
ArgumentCaptor
хранит и предоставляет все значения соответствующего параметра, с которыми метод был вызван до того, как данный ArgumentCaptor
был применён. getValue()
возвращает последнее полученное значения, getAllValues()
— все значения в порядке получения. Не очень удобно, что перехват параметра обязательно комбинируется с контролем количества вызовов, но это мелочь.
Mock-объекты как значения полей и аннотации Mockito
Если в классе теста есть поля, которым я хочу присвоить mock-объекты в качестве значений, это не обязательно делать вручную — достаточно снабдить его аннотацией @Mock
и до каких-либо обращений к нему выполнить вот такой вызов:
MockitoAnnotations.initMocks(this);
(несмотря на название, этот метод предназначен не только для mock'ов, а задействует также и все нижеперечисленные аннотации)
Для spy предусмотрена аннотация @Spy
— она в целом аналогична @Mock
… но для spy может использоваться объект, на основе которого он будет создан, помните? Такой объект можно сразу указать в качестве значения аннотируемого поля, но можно и не указывать — тогда spy будет создан на основе класса.
Есть аннотация @Captor
для создания экземпляров ArgumentCaptor
— о ней отдельно, пожалуй, больше ничего не скажешь.
Ещё существует @InjectMocks
. Помеченное таким образом поле инициализируется не каким-то исчадием Mockito, а самым что ни на есть настоящим объектом указанного класса. Его поля по возможности проинициализированы значениями mock-полей моего тестового класса, помеченных соответствующей аннотацией. Для этого используется конструктор с наибольшим числом параметров, сеттеры и так далее. Если какого-то объектного параметра конструктора не хватает, вместо него будет использован null
, а вот параметр-примитив просто не позволит тесту сработать. В целом это похоже на маленькую и простую (и всё равно не такую уж примитивную) реализацию dependency injection.
Откат поведения к дефолтному и сессии Mockito
Если в моём тестовом классе всего один тестирующий метод, всё отлично: я создал mock (spy, argument captor...), задал ему поведение, использовал его в тесте, всё. Но если их больше, а mock'и — это поля тестового класса, всё может оказаться сложнее. JUnit создаёт отдельный экземпляр тестового класса при вызове каждого из тестирующих методов, и у него здесь проблем нет, а вот у TestNG другой подход — экземпляр создаётся лишь один на все вызовы. Соответственно, поведение, определённое для mock'ов в одном из методов, будет воспроизводиться и в других, выполняемых после него, количество вызовов методов будет суммарным для них и т. д. А это, скорее всего, нежелательно — тем более, что порядок выполнения тестовых методов в общем случае не гарантирован.
Чтобы этого избежать, нужно до вызова каждого тестирующего метода тем или иным способом привести все mock-объекты в состояние по умолчанию. У TestNG есть для этого аннотации @BeforeMethod
(и @AfterMethod
для постобработки). После этого часто бывает удобно заново задать желаемое поведение для mock'ов в той степени, в которой оно общее для всех тестовых методов, чтобы на долю самих методов осталось лишь задание специфических деталей (это актуально и для JUnit — у него есть аналогичная аннотация @Before
).
Простой и очевидный, но не очень удобный способ, — использовать методы Mockito.reset
и Mockito.clearInvocations
. Оба принимают varargs, и передавать им нужно соответствующие mock'и. Первый возвращает к дефолтовому поведение методов, второй сбрасывает счётчики вызовов. Этот подход несколько гибче других: в некоторых редких случаях (например, когда тестирующие методы следуют друг за другом в заданном порядке и составляют единый сценарий) может оказаться, что поведение и/или счётчики части mock'ов я откатывать не хочу, — тогда достаточно не передавать их соответствующему методу. Также он порой может пригодиться, чтобы вернуть поведение mock'а по умолчанию в ходе работы тестирующего метода. Впрочем. авторы не рекомендуют пользоваться этими методами, утверждая, что необходимость в их вызове внутри тестирующего метода указывает на низкое качество тестов.
При использовании аннотаций доступен другой способ (пожалуй, самый популярный) — просто вызывать каждый раз MockitoAnnotations.initMocks(this);
. Это позволит переинициализировать "начисто" все поля, помеченные аннотациями Mockito.
Ещё одно решение — использовать так называемые сессии Mockito. Именно его рекомендуют авторы. В начале сессии все mock-объекты инициализируются, а после работы обязательно должно быть выполнено её окончание (хотя mock'и продолжают оставаться функциональными и после него). Если я хочу создавать отдельную сессию для каждого тестового метода, то удобно создать поле типа MockitoSession
, присвоить ему значение до вызова тестового метода и завершить сессию после. Вот пример для случая TestNG:
@Mock
DataService dataService;
MockitoSession session;
@BeforeMethod
public void beforeMethod() {
session = Mockito.mockitoSession()
.initMocks(this)
.startMocking();
}
@Test
public void testMethod() {
// some code using the dataService field
}
@AfterMethod
public void afterMethod() {
session.finishMocking();
}
Кроме того, сессии позволяют контролировать ещё некоторые аспекты работы — в частности, отслеживают "лишние" (заданные, но не задействованные в тесте) варианты поведения, если это нужно.
Что ещё?
Выше я рассмотрел основные возможности Mockito: создание mock и spy-объектов, задание из поведения и наблюдение за их использованием. Это не весь функционал этой библиотеки, но основная и чаще всего используемая часть. Не упомянутыми, в частности, остались:
- настройка Mockito и отдельных mock-объектов при помощи
MockSettings
(а там есть достаточно любопытные вещи — например, можно заставить mock'и по умолчанию реализовать какие-то дополнительные интерфейсы); - работа с информацией о mock-объекте, хранящейся в
MockingDetails
; - использование класса
BDDMockito
как альтернативыMockito
; - интеграция с тестовыми фреймворками (классы для интеграции с JUnit есть непосредственно в составе основной библиотеки Mockito, есть и отдельные интеграционные библиотеки).
За освещением этих и других вопросов обращайтесь к официальной документации Mockito. Большая часть вышеизложенного более или менее полно описана непосредственно в javadoc'е класса Mockito
.
Вот, пожалуй, и всё.