PowerMock (+Mockito): новый взгляд на unit-тестирование

от автора

image
Качественный код невозможен без тестов. А качественные тесты — без моков. В создании моков нам давно помогают различные полезные библиотечки, наподобие 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 методов, внутренних, вложенных и анонимных классов и много чего еще. Но описанный выше функционал является, на мой взгляд, жизненно необходимым. С остальным вы можете разобраться сами или, если вам понравится мое изложение, я могу рассказать в другой статье.

ссылка на оригинал статьи http://habrahabr.ru/post/172239/


Комментарии

Добавить комментарий

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