Mockito и как его готовить

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

О статье


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


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


Содержание:


  1. Mockito: что это такое и зачем нужно
  2. Окружение, версии и подопытное животное
  3. mock и spy
  4. Управление поведением
    1. Задание условий вызова
    2. Задание результатов вызова
  5. Слежение за вызовами методов
  6. Mock-объекты как значения полей и аннотации Mockito
  7. Откат поведения к дефолтному и сессии Mockito
  8. Что ещё?

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, позволяющей вызвать один из методов семейства Mockito.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);

Бросить исключения ничуть не сложнее:


Mockito.when(dataService.getDataById("invalidId"))
       .thenThrow(new IllegalArgumentException());

Есть и другой способ: можно создать объект исключения и бросить непосредственно его, а можно предоставить Mockito только класс исключения, чтобы оно было создано автоматически:


Mockito.when(dataService.getDataById("invalidId"))
       .thenThrow(IllegalArgumentException.class);

В обоих случаях синтаксис позволяет использовать и checked исключения, однако Mockito не позволит запустить такой тест, если тип исключения не соответствует методу, который я хочу заставить бросить это исключение.


При использовании класса как параметра конструкторы (даже без параметров), а равно и прямая инициализация полей, игнорируются — объект создаётся в обход них (в конце концов, это же Mockito!), так что все поля брошенного исключения будут равны null. Поэтому, если для вас имеет значение содержимое исключения (допустим, какое-нибудь поле type, у которого есть значение по умолчанию), придётся отказаться от этого способа и создавать исключения вручную.


Эти варианты реакции подходят, если в ответ на вызов с заданными условиями нужно всегда возвращать определённое, всегда одно и то же значение результата или выбрасывать всегда одинаковое исключение, и в большинстве случаев этих возможностей вполне достаточно. Но как быть, если требуется бОльшая гибкость? Предположим, мой метод принимает коллекцию значений, а возвращает другую коллекцию значений, связанных с первыми одно к одному (например, это получение коллекции объектов данных по набору их ID), и я хочу в рамках теста использовать этот mock-объект неоднократно с разными наборами входных данных, получая каждый раз соответствующий результат. Можно, конечно, описать по отдельности реакцию на каждый конкретный набор параметров, но есть более удобное решение — метод Mockito.thenAnswer, он же Mockito.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'и которых мы и создаём), обладающих нужным нам предсказуемым поведением. Может возникнуть другая, в некотором смысле обратная задача: убедиться в том, что тестируемый класс вызывает методы этих объектов нужное число раз, в нужном порядке и с нужными параметрами. Для этого предназначены методы семейства verify.


Простейший вариант, когда я проверяю факт однократного вызова метода на протяжении выполнения теста, выглядит так:


Mockito.verify(dataService).getDataById(Mockito.any());

Тест с такой конструкцией пройдёт успешно, если она находится после единственного за время выполнения теста вызова метода getDataById, и упадёт, если метод не был вызван или был вызван дважды и более. Заметьте, что непосредственно в этой конструкции метод у самого 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'и — это поля тестового класса, инициализация которых была выполнена лишь единоды (скажем, в случае TestNG — методом, аннотированным @BeforeClass), то заданное в одном методе поведение будет оставаться в силе и в других, выполняемых после него. Это, скорее всего, не нужно — тем более, что порядок выполнения тестовых методов в общем случае не гарантирован.


Чтобы этого избежать, нужно до вызова каждого тестового метода (непосредственно перед его вызовом либо после вызова предыдущего; в частности, у TestNG есть для этого аннотации @BeforeMethod и @AfterMethod) тем или иным способом привести все mock-объекты в состояние по умолчанию. После этого часто бывает удобно заново задать желаемое поведение для mock'ов в той степени, в которой оно общее для всех тестовых методов, чтобы на долю самих методов осталось лишь задание специфических деталей.


Простой и очевидный, но не очень удобный способ, — передать все mock'и методу Mockito.reset. После этого они "забудут" всё, чему я их до этого "научил". Этот подход несколько гибче других: в некоторых редких случаях (например, когда тестирующие методы следуют друг за другом в заданном порядке и составляют единый сценарий) может оказаться, что поведение части mock'ов я откатывать не хочу, — тогда достаточно не передавать их этому методу. Также он порой может пригодиться, чтобы вернуть поведение mock по умолчанию в ходе работы тестирующего метода. Впрочем. авторы не рекомендуют пользоваться Mockito.reset, утверждая, что необходимость в его вызове внутри тестирующего метода указывает на низкое качество тестов.


При использовании аннотаций доступен другой способ (пожалуй, самый популярный) — просто вызывать каждый раз MockitoAnnotations.initMocks(this);. Это позволит переинициализировать "начисто" все поля, помеченные аннотациями Mockito.


Ещё одно решение — использовать так называемые сессии Mockito. Именно его рекомендуют авторы. В начале сессии все 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.


Вот, пожалуй, и всё.

Интересные статьи

Интересные статьи

Привет, Хабр! Представляю вашему вниманию перевод статьи «The Government’s Secret UFO Program Funded Research on Wormholes and Extra Dimensions» автора Sarah Emerson. Министерство обороны США ...
Привет, народ! Представляю вам разработанный мной прототип кошачьих глаз. Конечно, проект еще не идеальный (находится на стадии доработки), но успешно работает. Задумка была – создать робота...
Чем функциональные компоненты React отличаются от компонентов, основанных на классах? Уже довольно давно традиционный ответ на этот вопрос звучит так: «Применение классов позволяет пользоваться б...
Наверняка большинство из вас нет-нет да и встречало в научно-популярной литературе упоминания о «многомировой интерпретации» квантовой механики (ММИ). Ее любят помянуть и в комментариях на Хабре,...
Linux Foundation открыли новое направление — CHIPS Alliance. В рамках этого проекта организация будет развивать свободную систему команд RISC-V и технологии для создания процессоров на её основе....