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

в 8:32, , рубрики: java, junit, mockito, PowerMock, 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 методов, внутренних, вложенных и анонимных классов и много чего еще. Но описанный выше функционал является, на мой взгляд, жизненно необходимым. С остальным вы можете разобраться сами или, если вам понравится мое изложение, я могу рассказать в другой статье.

Автор: ISergius

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js