Как стать автором
Обновить

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

Время на прочтение21 мин
Количество просмотров315K

О статье


Перед вами очередное руководство по 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, позволяющий вызвать один из методов семейства .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.


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

Теги:
Хабы:
Всего голосов 16: ↑16 и ↓0+16
Комментарии11

Публикации

Истории

Работа

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань