Качественный код невозможен без тестов. А качественные тесты — без моков. В создании моков нам давно помогают различные полезные библиотечки, наподобие EasyMock или Mockito. В своей практике я использую Mockito, как самое гибкое, красивое и функциональное средство. Но, к сожалению, Mockito тоже не стал серебрянной пулей. Ограничением всегда являлись final классы, private поля и методы, static методы и многое другое. И приходилось выбирать: или красивый дизайн, или качественное покрытие тестами. Меня, как приверженца красивой архитектуры и качественных тестов, такой расклад не устраивал. И вот совсем недавно я наткнулся на замечательную библиотечку — PowerMock, которая удовлетворила практически все мои запросы. За исключением одного, но об этом позже.
Итак, преступим. Для работы нам понадобятся: знание Java, JUnit, Mockito. Все это добро будет вариться в простом Maven проекте (надеюсь, этим уже никого не удивишь).
Для начала убедимся, что в проект добавлена зависимость JUnit не ниже 4 версии. Конечно, можно все сконфигурить и использовать и с более старыми версиями. Но мы все будем делать на самых последних версиях. Теперь добавим Mockito & PowerMock. Должно получиться что то вроде этого:
<properties>
<junit.version>4.11</junit.version>
<mockito.version>1.9.5</mockito.version>
<powermock.version>1.5</powermock.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mockito.version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Все готово к работе. Начинаем! Я не буду выдумывать какую то задачу, аля «Hello World!» или «Pet Clinic». Разберем ситуационно на сферических примерах. Те, кто имеет большой опыт написания тестов сразу увидят, как это можно применить. А те, кто еще только начинает… Поймут все, когда столкнутся с подобными ситуациями на практике.
Начнем с простого. Где то в недрах нашего гениального кода используется final класс, вызов метода которого нам необходимо проверить. Mockito бессильно, у этого класса нет интерфейса, а сам класс не может иметь наследников. Что либо изменить мы тоже не можем — или в силу архитектурных особенностей, или в силу того, что это сторонний сервис. Код для наглядности:
// сторонний класс
public final class ExternalService {
public void doMegaWork() {
// очень полезные действия,
// которые сами мы ни за что не реализуем =)
}
}
// наш класс
public class InternalService {
private final ExternalService externalService;
public InternalService(final ExternalService externalService) {
this.externalService = externalService;
}
public void doWork() {
externalService.doMegaWork();
}
}
Что бы не городить огород, воспользуемся замечательной возможностью PowerMock'а. Тест будет выглядеть так:
@RunWith(PowerMockRunner.class)
@PrepareForTest({ ExternalService.class })
public class InternalServiceTest {
private final ExternalService externalService = PowerMockito.mock(ExternalService.class);
private final InternalService internalService = new InternalService(externalService);
@Before
public void before() {
Mockito.reset(externalService);
}
@Test
public void doWorkTest() {
internalService.doWork();
Mockito.verify(externalService).doMegaWork();
}
}
Запускаем тест — все работает! Разберемся, что тут к чему. Первое, на что бросается взгляд — аннотации @RunWith & @PrepareForTest. Первая необходима, что бы заменить стандартный JUnit исполнитель тестов на PowerMock'овский, который использует магию класслоадера, что бы решить проблему создания mock-бъекта из final класса. Вторая аннотация подсказывает исполнителю теста, какие классы необходимо подготовить для теста. Далее мы видим, что для создания mock-объекта мы используем фактори метод из набора PowerMockito. Вот и все!
Еще одна простая и интересная возможность — проверять вызовы static методов. Листинг:
// сторонний сервис
public class StaticService {
public static void doStatic() {
//
}
public static String doStaticWithParams(final Object obj) {
return "";
}
}
// наш сервис
public class UseStaticService {
public String useStatic(final Object obj) {
StaticService.doStatic();
//
return StaticService.doStaticWithParams(obj);
}
}
// тест нашего сервиса
@RunWith(PowerMockRunner.class)
@PrepareForTest({ StaticService.class })
public class UseStaticServiceTest {
private static final Object OBJECT_PARAM = new Object();
private static final String RETURN_STRING = "result";
private final UseStaticService useStaticService = new UseStaticService();
public UseStaticServiceTest() {
PowerMockito.mockStatic(StaticService.class);
PowerMockito.when(StaticService.doStaticWithParams(OBJECT_PARAM)).thenReturn(RETURN_STRING);
}
@Test
public void useStaticTest() {
String result = useStaticService.useStatic(OBJECT_PARAM);
PowerMockito.verifyStatic();
StaticService.doStatic();
PowerMockito.verifyStatic();
StaticService.doStaticWithParams(OBJECT_PARAM);
assertEquals(RETURN_STRING, result);
}
}
Аннотации @RunWith & @PrepareForTest так же необходимы для работы со static методами. Рассмотрим, для чего необходимы новые инструкции:
PowerMockito.mockStatic(Class<?> type) — создает mock для всех статик методов в заданном классе. Стоит отметить, что можно создать mock только для необходимых методов. Как — разберетесь сами ;)
PowerMockito.when(T methodCall).thenReturn(returnValue) — стандартный способ задать некое поведение созданной заглушке.
PowerMockito.verifyStatic() — вызывается перед проверкой каждого статического вызова метода.
ExternalMegaService.doStatic() — определяет, какой собственно метод должен был быть вызван.
Еще одна замечательная возможность PowerMock'а — mock'ать создание новых объектов. Рассмотрим такой вот сферический пример:
// фабрика, создающая внешний сервис
public final class ExternalServiceFactory {
public ExternalService createExternalService() {
return new ExternalService();
}
}
// наш сервис, который использует фабрику для получения внешнего сервиса
public class InternalService {
private final ExternalServiceFactory externalServiceFactory;
public InternalService(final ExternalServiceFactory externalServiceFactory) {
this.externalServiceFactory = externalServiceFactory;
}
public void doWork() {
externalServiceProvider.createExternalService.doMegaWork();
}
}
// и, собственно, тест
@RunWith(PowerMockRunner.class)
@PrepareForTest({ ExternalServiceFactory.class, ExternalService.class })
public class InternalServiceTest {
private final ExternalService externalService = PowerMockito.mock(ExternalService.class);
private final ExternalServiceFactory externalServiceFactory;
private final InternalService internalService;
public InternalServiceTest() throws Exception {
PowerMockito.whenNew(ExternalService.class)
.withNoArguments()
.thenReturn(externalService);
externalServiceFactory = new ExternalServiceFactory();
internalService = new InternalService(externalServiceFactory);
}
@Before
public void before() {
Mockito.reset(externalService);
}
@Test
public void doWorkTest() {
internalService.doWork();
Mockito.verify(externalService).doMegaWork();
}
}
Конструкция PowerMockito.whenNew(Class<?> type).withNoArguments().thenReturn(instance) говорит PowerMock'у заменить в инспектируемых классах создание объектов типа type на объект instance. Важно, что бы объект, в котором необходимо заменить создание mock объекта, создавался после этой конструкции. Так же следует отметить, что ExternalServiceFactory может являться не обычным объектом, а partial mock'ом (spy) и тогда его поведение тоже можно будет проверить.
Неприятной ложкой дегтя является то, что если вам необходимо проинструктировать класс (@PrepareForTest), который вы тестируете (например, что бы проинициализировать моками статики), то вы никогда не узнаете степень покрытия данного класса тестами, т.к. coverage тул не сможет его проинспектировать. В таких случаях я разделяю тест на два класса. В первом проверяю все, что можно проверить без инструктирования тестируемого класса, во втором — только то, для чего необходимо делать @PrepareForTest.
Вот такие замечательные возможности для тестирования предоставляет PowerMock. У него есть еще и масса других фишечек, таких как мокирование private методов, внутренних, вложенных и анонимных классов и много чего еще. Но описанный выше функционал является, на мой взгляд, жизненно необходимым. С остальным вы можете разобраться сами или, если вам понравится мое изложение, я могу рассказать в другой статье.
Автор: ISergius