Думаю, многим довелось выпить какой-нибудь напиток, который глубоко впился в наши вкусовые рецепторы, что нам хочется пить его каждый день. Так вот для меня таким «напитком» стал Mockito. Один раз написав простенький тест, можно вернуть себе веру в магию. Я всё ещё помню, как удивлялся тому, как он работает.

Чему же я удивился? Например, этому:

private static class Apple {
   private String color;
   public String getName() {return color;}
}

@Test
public void basic() {
   Apple apple = mock(Apple.class);
   when(apple.getName()).thenReturn("Red");
   assertEquals("Red", apple.getName()); // true
}

С точки зрения написания кода, это очень красиво и понятно:

  • Мы создаём экземпляр-заглушку для класса Apple.
  • Затем мы как бы говорим, когда вызывается метод apple.getColor(), то верни «Red».
  • Далее мы просто проверяем действительно ли apple.getColor() возвращает то, что мы хотим, и это работает!

Внимание! Не читайте дальше, если и дальше хотите верить в магию. Дальнейшее содержание статьи отнимет у вас и эту толику детского счастья.



Что такое Mockito?


Mockito является открытой библиотекой с лицензией MIT License.

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

Библиотека довольно популярна и часто используется при тестировании, например, Spring Boot приложений.

Чуть-чуть отойдём от темы и представим себе волшебника, который взмахом палочки по воздуху переместил яблоко с одного стола на другой. Согласитесь, результат для нас вполне очевиден. Адам может и сам перенести райское яблоко с одного стола на другой. Тем не менее благодаря самому процессу как яблоко пролетело по воздуху – именно это делает его магией.

Так и здесь. Результат для нас может вполне очевиден, но как же он реализован?

Присмотримся к коду ещё раз и акцентируем внимание на второй строке:

when(apple.getName()).thenReturn("Red");

Внутри метода when по сути мы помещаем возвращаемое значение из метода getName(), а потом говорим thenReturn. Но как так получается? Как when может изменить наш метод getName, просто получив то значение, которое он возвращает?

Более того, мы знаем что поле name не инициализировано, а значит, там будет null.

За это уже можно влюбиться в неё и начать вовсю использовать.

По моему личному мнению, Mockito – полезная вещь, которую стоит изучить при наличии свободного времени. Но если изучить досконально не получится – тоже не страшно, то, что вы будете иметь о ней представление, – уже хорошо. Тем более лучшие практики подтверждают, что если ваш код трудно тестировать, то с вашим кодом явно что-то не так. В пользу ознакомления, а не глубокого изучения, говорит и то, что в 2018 году появился такой инструмент, как TestContainers, который умеет поднимать реальную базу в Docker и реально выполнять запросы в БД/Kafka/RabbitMQ.

Егор Воронянский, Java Middle Developer в BPC Banking Technologies.
Ментор раздела backend на курсе SkillFactory Java-разработка

Учимся показывать фокусы


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

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

Нашим же приготовлением был вызов метода mock:

Apple apple = mock(Apple.class);

Отсюда мы понимаем, что наш apple – это уже необычное яблоко.

И действительно, если вы попробуете передать обычный экземпляр класса в метод when, то получите ошибку.

Попробуем узнать, кем оно теперь является:

@Test
public void basic2() {
   Apple apple = mock(Apple.class);
   System.out.println(apple.getClass().getCanonicalName());
   // org.own.ArticleExample1$Apple$MockitoMock$692964397
   System.out.println(apple.getClass().getSuperclass().getCanonicalName());
   // org.own.ArticleExample1.Apple

   assertNotEquals(apple.getClass(), Apple.class); // true

}

Итак, с кем же мы имеем дело? Кто же этот самозванец, который выдаёт себя за наше яблоко?

Обычно такими делами занимается паттерн Proxy. Суть его в том, чтобы подсунуть нам не конкретно наш экземпляр, который мы хотим получить (в нашем случае Apple), а его наследника, который может иметь какие-либо необходимые модификации.

Что касается именно нашей библиотеки Mockito, то она действительно основана на проксировании. Тем не менее есть библиотеки, занимающиеся тем же, но основанные не на проксировании, а, например, на изменении байт-кода. Примером такой библиотеки может служить PowerMock с лицензией Apache-2.0 License.

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

Далее мы разберём, в чём же минусы решения через проксирование.

Proxy


На прокси, с одной стороны, можно смотреть как на заменителя экземпляра какого-то типа, а с другой – как на смотрящий или проверяющий за происходящими вызовами метод.

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

Индейская пословица гласит: 

Скажи мне – и я забуду, покажи мне – и я не смогу запомнить, привлеки меня к участию – и я пойму.

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

ProxyExample

import java.lang.reflect.Proxy;

/**
 * @author Arthur Kupriyanov
 */
public class ProxyExample {
	interface IApple {
		String getColor();
	}
	private static class Apple implements IApple {
		private String color = "red";
		public String getColor() {
			return color;
		}
	}

	public static void main(String[] args) {
		Object proxyInstance = Proxy.newProxyInstance(
				ProxyExample.class.getClassLoader(),
				Apple.class.getInterfaces(), (proxy, method, args1) -> {
					System.out.println("Called getColor() method on Apple");
					return method.invoke(new Apple(), args1);
				});

		IApple appleProxy = (IApple) proxyInstance;
		System.out.println(appleProxy.getColor());
	}
}

На выводе мы получим:

Called getColor() method on Apple
red

Попробуйте, например, вместо вызова method.invoke сделать return «Green».

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

Теперь из-за принципов работы Proxy мы сразу можем понять, какие ограничения на него наложены:

  • Мы не можем «перехватывать» вызовы статических, приватных и ненаследуемых (final) методов.
  • Сделать прокси из ненаследуемого класса (final).

Показываем фокус


На самом деле, имея лишь этот арсенал, можно сделать что-то наподобие Mockito.

Конечно, наш фокус будет весьма ограничен по сравнению с увиденным ранее, но основной метод фокуса будет тем же.

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

  1. Создадим прокси типа с нашим InvocationHandler с необходимой нам логикой.
  2. Создадим метод when, который вместе с thenReturn запомнит вызов метода, его аргументы и значение, которое нужно вернуть.
  3. При вызове метода из нашего прокси объекта будем проверять сохранённые значения из пункта 2. Если есть сохранение заданного метода с такими же аргументами, то вернём сохранённое значение. Если нет, то вернем null.

Исходный код можно найти в репозитории или же поиграть в онлайн-редакторе.

private static MockInvocationHandler lastMockInvocationHandler;

@SuppressWarnings("unchecked")
public static <T> T mock(Class<T> clazz) {
   MockInvocationHandler invocationHandler = new MockInvocationHandler();
   T proxy = (T) Proxy.newProxyInstance(Bourbon.class.getClassLoader(), new Class[]{clazz}, invocationHandler);
   return proxy;
}

public static <T> When<T> when(T obj) {
   return new When<>();
}

public static class When<T> {
   public void thenReturn(T retObj) {
	   lastMockInvocationHandler.setRetObj(retObj);
   }
}

private static class MockInvocationHandler implements InvocationHandler {

   private Method lastMethod;
   private Object[] lastArgs;
   private final List<StoredData> storedData = new ArrayList<>();

   private Optional<Object> searchInStoredData(Method method, Object[] args) {
	   for (StoredData storedData : this.storedData) {
		   if (storedData.getMethod().equals(method) && Arrays.deepEquals(storedData.getArgs(), args)) {
			   // если данные есть, то возвращаем сохраненный
			   return Optional.ofNullable(storedData.getRetObj());
		   }
	   }

	   return Optional.empty();
   }

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) {
	   lastMockInvocationHandler = this;
	   lastMethod = method;
	   lastArgs = args;

	   // проверяем в сохраненных данных
	   return searchInStoredData(method, args).orElse(null);

   }

   public void setRetObj(Object retObj) {
	   storedData.add(new StoredData(lastMethod, lastArgs, retObj));
   }

   private static class StoredData {
	   private final Object[] args;
	   private final Method method;
	   private final Object retObj;

	   private StoredData(Method method, Object[] args, Object retObj) {
		   this.args = args;
		   this.method = method;
		   this.retObj = retObj;
	   }

	   private Object[] getArgs() {
		   return args;
	   }

	   private Method getMethod() {
		   return method;
	   }

	   private Object getRetObj() {
		   return retObj;
	   }
   }

}

Этот код не претендует на какое-либо качество кода, но он вполне нагляден.

Основным классом здесь является MockInvocationHandler, в который мы вставляем всю нашу логику с поиском сохранённых значений.  Все сохранённые значения мы храним в виде списка StoredData, а затем при вызове любого из методов запроксированного объекта проверяем, было ли сохранено какое-либо значение.

Те сомнения, которые не разрешает теория, разрешит тебе практика. Людвиг Файербах

Поэтому смело открываем IDE, копируем код (думаю, в этом мы все профи), практикуемся и смотрим каждую строчку кода. Более ленивые могут открыть онлайн-редактор (ссылка также была наверху перед кодом) и редактировать уже там.

Заключение


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

Простота библиотеки в использовании и является ключевым фактором фокуса. Это выглядит так просто и очевидно при использовании, что кажется магией, хотя за кадром остаются тяжёлая подготовка (создание Proxy) и исполнение (поиск сохраненных значений).

Для ознакомления с методами использования библиотеки уже есть довольно много других статей на разных языках, в том числе на русском. Поэтому вам осталось самое интересное: изучить внутренности самого Mockito, (хорошо, что исходники есть в репозитории на GitHub) и понять, как он хорошо интегрируется в Spring Boot для тестирования. А я пожелаю вам удачи в этом пути!



image