Качественный код невозможен без тестов. А качественные тесты — без моков. В создании моков нам давно помогают различные полезные библиотечки, наподобие 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/
Добавить комментарий