Практическое руководство по модульному тестированию для повседневного Mockito

Опубликовано: 2022-03-11

Модульное тестирование стало обязательным в эпоху Agile, и существует множество инструментов, помогающих в автоматизированном тестировании. Одним из таких инструментов является Mockito, платформа с открытым исходным кодом, которая позволяет создавать и настраивать макеты объектов для тестов.

В этой статье мы рассмотрим создание и настройку макетов и их использование для проверки ожидаемого поведения тестируемой системы. Мы также немного углубимся во внутренности Mockito, чтобы лучше понять его дизайн и предостережения. Мы будем использовать JUnit в качестве фреймворка модульного тестирования, но поскольку Mockito не привязан к JUnit, вы можете следовать ему, даже если используете другой фреймворк.

Получение Мокито

В наши дни получить Mockito легко. Если вы используете Gradle, вам нужно добавить эту единственную строку в ваш скрипт сборки:

 testCompile "org.mockito:mockito−core:2.7.7"

Что касается таких, как я, которые все еще предпочитают Maven, просто добавьте Mockito в свои зависимости следующим образом:

 <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>

Конечно, мир намного шире, чем Maven и Gradle. Вы можете использовать любой инструмент управления проектами для извлечения артефакта Mockito jar из центрального репозитория Maven.

Приближаясь к Мокито

Модульные тесты предназначены для проверки поведения определенных классов или методов, не полагаясь на поведение их зависимостей. Поскольку мы тестируем наименьшую «единицу» кода, нам не нужно использовать реальные реализации этих зависимостей. Кроме того, мы будем использовать несколько разные реализации этих зависимостей при тестировании разных вариантов поведения. Традиционный, хорошо известный подход к этому заключается в создании «заглушек» — конкретных реализаций интерфейса, подходящих для данного сценария. Такие реализации обычно имеют жестко закодированную логику. Заглушка — это своего рода тестовый двойник. Другие виды включают подделки, макеты, шпионы, макеты и т. д.

Мы сосредоточимся только на двух типах тестовых двойников: «моки» и «шпионы», так как Mockito активно использует их.

Мокает

Что такое издеваться? Очевидно, это не то место, где вы высмеиваете своих коллег-разработчиков. Насмешка для модульного тестирования — это когда вы создаете объект, который реализует поведение реальной подсистемы контролируемым образом. Короче говоря, макеты используются в качестве замены зависимости.

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

По умолчанию Mockito предоставляет реализацию для каждого метода макета.

Шпионы

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

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

Создание простого примера

Давайте посмотрим на простую демонстрацию, для которой мы можем написать тесты. Предположим, у нас есть интерфейс UserRepository с единственным методом поиска пользователя по его идентификатору. У нас также есть концепция кодировщика паролей, который превращает обычный текстовый пароль в хэш пароля. И UserRepository , и PasswordEncoder являются зависимостями (также называемыми соавторами) UserService , внедренными через конструктор. Вот как выглядит наш демонстрационный код:

Пользовательский репозиторий
 public interface UserRepository { User findById(String id); }
Пользователь
 public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
ПарольЭнкодер
 public interface PasswordEncoder { String encode(String password); }
Пользовательская служба
 public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }

Этот пример кода можно найти на GitHub, поэтому вы можете загрузить его для ознакомления вместе с этой статьей.

Применение Мокито

Используя наш пример кода, давайте посмотрим, как применить Mockito и напишем несколько тестов.

Создание макетов

С Mockito создать макет так же просто, как вызвать статический метод Mockito.mock() :

 import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);

Обратите внимание на статический импорт для Mockito. В оставшейся части этой статьи мы неявно будем считать этот импорт добавленным.

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

Также обратите внимание, что методы equals() и hashCode() нельзя имитировать.

Создание шпионов

Чтобы создать шпиона, вам нужно вызвать статический метод Mockito spy() и передать ему экземпляр для слежки. Вызов методов возвращаемого объекта вызовет настоящие методы, если только эти методы не заглушены. Эти вызовы записываются, и факты этих вызовов могут быть проверены (см. дальнейшее описание verify() ). Делаем шпиона:

 DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));

Создание шпиона мало чем отличается от создания макета. Более того, все методы Mockito, используемые для настройки мока, применимы и для настройки шпиона.

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

Возвращаемые значения по умолчанию

Вызов mock(PasswordEncoder.class) возвращает экземпляр PasswordEncoder . Мы даже можем вызывать его методы, но что они вернут? По умолчанию все методы макета возвращают «неинициализированные» или «пустые» значения, например, нули для числовых типов (как примитивных, так и упакованных), false для логических значений и нули для большинства других типов.

Рассмотрим следующий интерфейс:

 interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }

Теперь рассмотрим следующий фрагмент, который дает представление о том, какие значения по умолчанию следует ожидать от методов макета:

 Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());

Методы заглушки

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

Mockito предлагает два способа заглушки. Первый способ — « когда вызывается этот метод, то делайте что-нибудь». Рассмотрим следующий фрагмент:

 when(passwordEncoder.encode("1")).thenReturn("a");

Он читается почти как английский: «Когда вызывается passwordEncoder.encode(“1”) , возвращайте a ».

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

 doReturn("a").when(passwordEncoder).encode("1");

Фрагмент с этим методом заглушки будет выглядеть так: «Возврат a когда метод encode() объекта passwordEncoder вызывается с аргументом 1 ».

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

Давайте кратко рассмотрим методы заглушек, предоставляемые Mockito. Мы включим оба способа заглушки в наши примеры.

Возвращаемые значения

thenReturn или doReturn() используются для указания значения, которое будет возвращено при вызове метода.

 //”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");

или

 //”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");

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

 //when when(passwordEncoder.encode("1")).thenReturn("a", "b");

или

 //do doReturn("a", "b").when(passwordEncoder).encode("1");

То же самое можно сделать с помощью следующего фрагмента:

 when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");

Этот шаблон также можно использовать с другими методами заглушки для определения результатов последовательных вызовов.

Возврат пользовательских ответов

then() , псевдоним thenAnswer() и doAnswer() достигают одной и той же цели, настраивая собственный ответ, который будет возвращен при вызове метода, например:

 when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");

или

 doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");

Единственный аргумент, который принимает thenAnswer() , — это реализация интерфейса Answer . Он имеет единственный метод с параметром типа InvocationOnMock .

Вы также можете сгенерировать исключение в результате вызова метода:

 when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });

…или вызвать реальный метод класса (не применимо к интерфейсам):

 Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());

Вы правы, если думаете, что это выглядит громоздко. Mockito предоставляет thenCallRealMethod() и thenThrow() , чтобы упростить этот аспект вашего тестирования.

Вызов реальных методов

Как следует из названия, thenCallRealMethod() и doCallRealMethod() вызывают настоящий метод фиктивного объекта:

 Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());

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

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

 when(passwordEncoder.encode("1")).thenCallRealMethod();

Mockito завершится ошибкой со следующим сообщением:

 Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();

Спасибо разработчикам Mockito за то, что они позаботились о таких подробных описаниях!

Генерация исключений

thenThrow() и doThrow() настраивают фиктивный метод для создания исключения:

 when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());

или

 doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");

Mockito гарантирует, что выбрасываемое исключение действительно для этого конкретного заглушенного метода, и будет жаловаться, если исключение не находится в списке проверенных исключений метода. Рассмотрим следующее:

 when(passwordEncoder.encode("1")).thenThrow(new IOException());

Это приведет к ошибке:

 org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException

Как видите, Mockito обнаружил, что encode() не может генерировать IOException .

Вы также можете передать класс исключения вместо передачи экземпляра исключения:

 when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);

или

 doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");

Тем не менее, Mockito не может проверять класс исключения так же, как он проверяет экземпляр исключения, поэтому вы должны соблюдать дисциплину и не передавать недопустимые объекты класса. Например, следующее вызовет исключение IOException , хотя ожидается, что encode() не вызовет проверенное исключение:

 when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");

Имитационные интерфейсы с методами по умолчанию

Стоит отметить, что при создании мока для интерфейса Mockito имитирует все методы этого интерфейса. Начиная с Java 8, интерфейсы могут содержать методы по умолчанию наряду с абстрактными. Эти методы также имитируются, поэтому вам нужно позаботиться о том, чтобы они действовали как методы по умолчанию.

Рассмотрим следующий пример:

 interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());

В этом примере assertFalse() завершится успешно. Если это не то, что вы ожидали, убедитесь, что Mockito вызвал настоящий метод, например:

 AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());

Сопоставители аргументов

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

Однако иногда мы не знаем эти значения заранее.

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

Рассмотрим следующий фрагмент:

 when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));

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

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

 abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);

Чтобы исправить ошибку, мы должны заменить последнюю строку, чтобы включить сопоставитель аргумента eq для a , как показано ниже:

 when(mock.call(eq("a"), anyInt())).thenReturn(true);

Здесь мы использовали сопоставители аргументов eq() и anyInt() , но доступно и много других. Полный список сопоставителей аргументов см. в документации по классу org.mockito.ArgumentMatchers .

Важно отметить, что вы не можете использовать сопоставители аргументов вне проверки или заглушки. Например, вы не можете иметь следующее:

 //this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);

Mockito обнаружит неуместный сопоставитель аргументов и InvalidUseOfMatchersException . Проверка с помощью сопоставителей аргументов должна выполняться следующим образом:

 verify(mock).encode(or(eq("a"), endsWith("b")));

Сопоставители аргументов также не могут использоваться в качестве возвращаемого значения. Mockito не может вернуть anyString() или что-то еще; точное значение требуется при заглушке вызовов.

Пользовательские сопоставления

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

Таким образом, стоит проверить, можете ли вы упростить тест, используя некоторые мягкие сопоставители аргументов, такие как isNull() и nullable() , прежде чем писать собственный сопоставитель. Если вы все еще чувствуете необходимость написать средство сопоставления аргументов, Mockito предоставляет для этого семейство методов.

Рассмотрим следующий пример:

 FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));

Здесь мы создаем сопоставитель аргументов hasLuck и используем argThat() для передачи сопоставителя в качестве аргумента фиктивному методу, заглушая его, чтобы он возвращал true , если имя файла заканчивается на «удача». Вы можете рассматривать ArgumentMatcher как функциональный интерфейс и создать его экземпляр с лямбдой (что мы и сделали в примере). Менее лаконичный синтаксис будет выглядеть так:

 ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };

Если вам нужно создать сопоставитель аргументов, который работает с примитивными типами, для этого есть несколько других методов в org.mockito.ArgumentMatchers :

  • charThat (сопоставитель ArgumentMatcher<Character>)
  • booleanThat (сопоставитель ArgumentMatcher<Boolean>)
  • byteThat (сопоставитель ArgumentMatcher<Byte>)
  • shortThat (сопоставитель ArgumentMatcher<Short>)
  • intThat (сопоставитель ArgumentMatcher<Integer>)
  • longThat (сопоставитель ArgumentMatcher<Long>)
  • floatThat (сопоставитель ArgumentMatcher<Float>)
  • doubleThat (сопоставитель ArgumentMatcher<Double>)

Объединение сопоставителей

Не всегда стоит создавать собственный сопоставитель аргументов, если условие слишком сложно для обработки с помощью базовых сопоставителей; иногда комбинирование сопоставителей помогает. Mockito предоставляет сопоставители аргументов для реализации общих логических операций («не», «и», «или») над сопоставителями аргументов, которые соответствуют как примитивным, так и непримитивным типам. Эти сопоставители реализованы как статические методы в классе org.mockito.AdditionalMatchers .

Рассмотрим следующий пример:

 when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));

Здесь мы объединили результаты двух сопоставителей аргументов: eq("1") и contains("a") . Окончательное выражение or(eq("1"), contains("a")) может быть интерпретировано как "строка аргумента должна быть равна "1" или содержать "a".

Обратите внимание, что в классе org.mockito.AdditionalMatchers перечислены менее распространенные сопоставители, такие как geq() , leq() , gt() и lt() , которые представляют собой сравнения значений, применимые для примитивных значений и экземпляров java.lang.Comparable .

Проверка поведения

После того, как имитация или шпион были использованы, мы можем verify , имели ли место конкретные взаимодействия. Буквально мы говорим: «Эй, Мокито, убедись, что этот метод вызывается с этими аргументами».

Рассмотрим следующий искусственный пример:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");

Здесь мы создали макет и вызвали его метод encode() . Последняя строка проверяет, что метод encode() макета был вызван с конкретным значением аргумента a . Обратите внимание, что проверка заглушенного вызова является избыточной; цель предыдущего фрагмента — показать идею выполнения проверки после некоторых взаимодействий.

Если мы изменим последнюю строку, чтобы она имела другой аргумент — скажем, b — предыдущий тест завершится ошибкой, и Mockito будет жаловаться, что фактический вызов имеет другие аргументы ( b вместо ожидаемого a ).

Сопоставители аргументов можно использовать для проверки точно так же, как и для заглушек:

 verify(passwordEncoder).encode(anyString());

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

 // verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());

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

 usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");

Этот тест завершится успешно, если encode() будет вызван и завершится в течение 500 миллисекунд или меньше. Если вам нужно подождать весь указанный вами период, используйте after() вместо timeout() :

 verify(passwordEncoder, after(500)).encode("a");

Другие режимы проверки ( times() , atLeast() и т. д.) можно комбинировать с timeout() и after() для выполнения более сложных тестов:

 // passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");

Помимо times() , поддерживаемые режимы проверки включают only() , atLeast() и atLeastOnce() (как псевдоним atLeast(1) ).

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

 PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");

Если мы изменим порядок имитируемых вызовов, тест завершится с ошибкой VerificationInOrderFailure .

Отсутствие вызовов также можно проверить с помощью verifyZeroInteractions() . Этот метод принимает макет или макеты в качестве аргумента и завершится ошибкой, если были вызваны какие-либо методы переданных макетов.

Также стоит упомянуть метод verifyNoMoreInteractions() , так как он принимает макеты в качестве аргумента и может использоваться для проверки того, что каждый вызов этих макетов был проверен.

Сбор аргументов

Помимо проверки того, что метод был вызван с определенными аргументами, Mockito позволяет вам захватить эти аргументы, чтобы вы могли позже запускать для них собственные утверждения. Другими словами, вы говорите: «Эй, Мокито, подтверди, что этот метод был вызван, и дай мне значения аргументов, с которыми он был вызван».

Давайте создадим макет PasswordEncoder , вызовем encode() , захватим аргумент и проверим его значение:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());

Как видите, мы передаем passwordCaptor.capture() в качестве аргумента encode() для проверки; это внутренне создает средство сопоставления аргументов, которое сохраняет аргумент. Затем мы извлекаем захваченное значение с помощью passwordCaptor.getValue() и проверяем его с помощью assertEquals() .

Если нам нужно захватить аргумент для нескольких вызовов, ArgumentCaptor позволяет получить все значения с помощью getAllValues() , например:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());

Тот же метод можно использовать для захвата аргументов метода переменной арности (также известного как varargs).

Тестирование нашего простого примера

Теперь, когда мы знаем о Mockito гораздо больше, пришло время вернуться к нашей демонстрации. Напишем тест метода isValidUser . Вот как это может выглядеть:

 public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }

Погружение под API

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

Давайте посмотрим, что происходит внутри Mockito при запуске следующего фрагмента кода:

 // 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));

Очевидно, первая строка создает mock. Mockito использует ByteBuddy для создания подкласса данного класса. Новый объект класса имеет сгенерированное имя, такое как demo.mockito.PasswordEncoder$MockitoMock$1953422997 , его equals() будет действовать как проверка личности, а hashCode() будет возвращать хэш-код идентификации. Как только класс сгенерирован и загружен, его экземпляр создается с помощью Objenesis.

Давайте посмотрим на следующую строку:

 when(mock.encode("a")).thenReturn("1");

Порядок важен: первый выполняемый здесь оператор — mock.encode("a") , который вызовет encode() для макета с возвращаемым значением по умолчанию null . Так что на самом деле мы передаем null в качестве аргумента when() . Mockito не важно, какое именно значение передается в when() , потому что он сохраняет информацию о вызове фиктивного метода в так называемой «постоянной заглушке» при вызове этого метода. Позже, когда мы вызываем when() , Mockito извлекает этот текущий объект-заглушку и возвращает его как результат when() . Затем мы вызываем thenReturn(“1”) для возвращаемого текущего объекта-заглушки.

Третья строка, mock.encode("a"); просто: мы вызываем заглушенный метод. Внутри Mockito сохраняет этот вызов для дальнейшей проверки и возвращает заглушенный ответ на вызов; в нашем случае это строка 1 .

В четвертой строке ( verify(mock).encode(or(eq("a"), endsWith("b"))); ) мы просим Mockito проверить, был ли вызов encode() с этими конкретные аргументы.

Verify verify() выполняется первым, что переводит внутреннее состояние Mockito в режим проверки. Важно понимать, что Mockito сохраняет свое состояние в ThreadLocal . Это позволяет реализовать хороший синтаксис, но, с другой стороны, это может привести к странному поведению, если инфраструктура используется неправильно (например, если вы попытаетесь использовать сопоставление аргументов вне проверки или заглушки).

Так как же Mockito создает сопоставление or ? Сначала вызывается eq("a") , и в стек сопоставителей добавляется сопоставитель equals . Во-вторых, endsWith("b") , и в стек добавляется сопоставитель endsWith . Наконец, вызывается or(null, null) — он использует два сопоставления, извлеченных из стека, создает сопоставление or и помещает его в стек. Наконец, вызывается encode() . Затем Mockito проверяет, что метод вызывался ожидаемое количество раз и с ожидаемыми аргументами.

Хотя сопоставители аргументов нельзя извлечь в переменные (поскольку это изменяет порядок вызовов), их можно извлечь в методы. Это сохраняет порядок вызовов и удерживает стек в правильном состоянии:

 verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }

Изменение ответов по умолчанию

В предыдущих разделах мы создавали наши моки таким образом, что при вызове любых имитируемых методов они возвращали «пустое» значение. Это поведение настраивается. Вы даже можете предоставить свою собственную реализацию org.mockito.stubbing.Answer , если те, что предоставлены Mockito, не подходят, но это может указывать на то, что что-то не так, когда модульные тесты становятся слишком сложными. Помните принцип KISS!

Давайте рассмотрим предложение Mockito предопределенных ответов по умолчанию:

  • RETURNS_DEFAULTS — стратегия по умолчанию; это не стоит упоминать явно при настройке макета.

  • CALLS_REAL_METHODS заставляет незаглушенные вызовы вызывать реальные методы.

  • RETURNS_SMART_NULLS позволяет избежать NullPointerException , возвращая SmartNull вместо null при использовании объекта, возвращаемого незаглушенным вызовом метода. Вы все равно потерпите неудачу с NullPointerException , но SmartNull даст вам более удобную трассировку стека со строкой, в которой был вызван незаглушенный метод. Это делает целесообразным, чтобы RETURNS_SMART_NULLS был ответом по умолчанию в Mockito!

  • RETURNS_MOCKS сначала пытается вернуть обычные «пустые» значения, затем имитирует, если возможно, и null в противном случае. Критерии пустоты немного отличаются от того, что мы видели ранее: вместо возврата null для строк и массивов макеты, созданные с помощью RETURNS_MOCKS возвращают пустые строки и пустые массивы соответственно.

  • RETURNS_SELF полезен для имитации сборщиков. С этой настройкой макет будет возвращать экземпляр самого себя, если вызывается метод, который возвращает что-то типа, равного классу (или суперклассу) класса, над которым выполняется макет.

  • RETURNS_DEEP_STUBS идет глубже, чем RETURNS_MOCKS и создает макеты, которые могут возвращать макеты из макетов из макетов и т. д. В отличие от RETURNS_MOCKS правила пустоты по умолчанию в RETURNS_DEEP_STUBS , поэтому он возвращает null для строк и массивов:

 interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());

Именование макета

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

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());

Mockito будет жаловаться, но поскольку мы официально не назвали макеты, мы не знаем, какой из них:

 Wanted but not invoked: passwordEncoder.encode(<any string>);

Назовем их, передав строку при построении:

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());

Теперь сообщение об ошибке более дружелюбно и явно указывает на robustPasswordEncoder :

 Wanted but not invoked: robustPasswordEncoder.encode(<any string>);

Реализация нескольких фиктивных интерфейсов

Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:

 PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);

Listening Invocations

A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.

 InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");

In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger (don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging() setting:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());

Take notice, though, that Mockito will call the listeners even when you're stubbing methods. Рассмотрим следующий пример:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");

This snippet will produce an output similar to the following:

 ############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"

Note that the first logged invocation corresponds to calling encode() while stubbing it. It's the next invocation that corresponds to calling the stubbed method.

Другие настройки

Mockito offers a few more settings that let you do the following:

  • Enable mock serialization by using withSettings().serializable() .
  • Turn off recording of method invocations to save memory (this will make verification impossible) by using withSettings().stubOnly() .
  • Use the constructor of a mock when creating its instance by using withSettings().useConstructor() . When mocking inner non-static classes, add an outerInstance() setting, like so: withSettings().useConstructor().outerInstance(outerObject) .

If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance() setting, so that Mockito will create a spy on the instance you provide, like so:

 UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));

When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.

Note that, when you create a spy, you're basically creating a mock that calls real methods:

 // creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));

When Mockito Tastes Bad

It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.

Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.

Clearing Invocations

Mockito allows for clearing invocations for mocks while preserving stubbing, like so:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);

Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.

Resetting a Mock

Resetting a mock with reset() is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.

Overusing Verify

Another bad habit is trying to replace every assert with Mockito's verify() . It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify() , while confirming the observable results of an executed action is done with asserts.

Mockito Is about Frame of Mind

Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.

With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.