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 методов, внутренних, вложенных и анонимных классов и много чего еще. Но описанный выше функционал является, на мой взгляд, жизненно необходимым. С остальным вы можете разобраться сами или, если вам понравится мое изложение, я могу рассказать в другой статье.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 12

    +6
    >И приходилось выбирать: или красивый дизайн, или качественное покрытие тестами
    Я думаю вам стоит выкинуть PowerMock и почитать про dependency injection… Моки на статик методы это зло
      +1
      А при чем тут dependency injection (который я, к слову, неплохо знаю) и статик методы? Существует множество сторонних библиотек с набором utility-классов со статическими функциями. Зачастую, в таких ситуациях можно обойтись и без использования моков. А иногда их очень не хватает. Здесь я не призывал использовать статики или делать моки на все подряд. Ко всему же надо подходить с головой.
        +1
        Подход «с головой» к пробелеме написания стабов для статик методов заключается в замене static method на instance method и DI.
        Можете привести пример где вам надо сделать mock на utility-классы со статическими функциями?
          0
          Научите заменять static метод в сторонней библиотеке? Пример… ByteBuffer.allocate()
            0
            Можете привести пример где вам надо сделать mock на utility-классы со статическими функциями?

            То есть не пример статической функции, это я и сам найду.
            А пример где для тестирование надо сделать мок статической функции
              +1
              Пример придуманный и совершенно сферический:

              public ByteBuffer serialize(Object obj) {
                  ByteBuffer buffer = ByteBuffer.allocate(new byte[100]);
                  serializer.serialize(object, buffer);
                  buffer.flip();
                  return buffer;
              }
              


              Опять же, всегда можно придраться, почему ByteBuffer не приходит извне функции. Но еще раз напоминаю, что это лишь пример, такой же сферический, как те, которые приведены в статье. Однако, похожие ситуации в жизни не раз бывали и в них не к чему было придраться. Просто на вскидку сложно вспомнить что то конкретное.
              Да, в этом примере очень бы хотелось проверить, выполняется ли flip() для буфера. Предположим, что если его кто то удалит, все будет плохо.
                +1
                Эмм, пример и правда слишком сферический, тут нечего даже сказать.
                Скорее всего тут не надо проверять что «вызван метод flip», а просто смотреть на возвращенный буфер и проверять его свойства.
                Все таки хотелось бы посмотреть какой был реальный кейс
                  +1
                  Есть два подхода к тестированию — BlackBox & WhiteBox. Если проверять только результат выполнения метода — это BlackBox тестирование. Если тестировать с проверкой всех «потрохов» — это WhiteBox подход. Мне позарез надо знать, что был вызван метод flip(), а не ручками выставлены значения. А то ведь всякое бывает.
                  А реальный кейс… Может Вы и сами скоро с подходящим столкнетесь. А если нет — тем лучше. Значит Вам конкретно эта функциональность PowerMock'а не пригодится. Но ведь важно просто знать, что возможность есть, что бы быть готовым.
                    0
                    Вот это вот «позарез надо знать» попахивает тем, что вы пытаетесь протестировать слишком много. Это приведёт к тому, что тестов будет слишком много, они будут слишком завязаны на коде, и как следствие, их придётся слишком часто менять при внутренних изменениях в коде, не влияющих на функциональность.

                    Тестировать надо функциональность, то есть «внешние проявления». Если вам позарез нужно знать, что был вызван метод flip(), значит, подумайте о том, что свалится, если этот метод не был вызван. И на эту ситуацию и напишите тест.
                      0
                      Вопрос о подходах unit-тестирования, а так же о том как и где их применять — это тема отдельной статьи и отдельного обуждения… В этой статье был обзор возможностей PowerMock для создания детализированных тестов.
                      И позвольте Вам возразить. Конечно, Вы частично правы. При таком подходе тесты будут завязаны на коде. И в случае изменений кода потребуется время на изменение тестов. Но это не всегда плохо. Упавший тест — повод задуматься: «А все ли я сделал правильно? Может кто то неспроста до меня сделал именно так?» Плюс ко всему, есть ряд ситуаций, когда тестирование по «внешним проявлениям» не подойдет. Простой пример: есть некий метод, предположим, public void open(), который как мы видим не принимает и не возвращает параметров. Внутри него происходит последовательный вызов нескольких функций из внутренних объектов (возможно так же и статические). Скорее всего, состояние тестируемого объекта изменится. Но, во-первых, это состояние может быть чисто внутренним и не торчать наружу. А следовательно, что бы его проверить необходимо производить танцы с бубном (или использовать PowerMock для удобного доступа к private полям). Во-вторых, в тестируемом методе наверняка важен порядок выполнения внутренних вызовов. Как Вы будете это проверять?
                      Думать о том, что свалится, если в этом методе что то будет не вызвано — это совсем другая задача, другого теста. Этот же метод вы оставите не протестированным.
                      Повторюсь, я утверждаю, что все тесты должны быть только такими. Все всегда зависит от ситуации. Просто всегда понимайте что вы делаете и что это может повлечь. Вы тестируете систему детально — будьте готовы к переделке тестов при рефакторинге архитектуры. Вы тестируете систему интеграционно в виде BlackBox — готовьтесь к тому, что внутри система может вести себя не корректно, а ваши тест кейсы просто не способны отловить эту особенность. И никогда не стоит говорить: «я крут и со мной такого не случится». Практика показывает, что случится. Причем в самый не подходящий момент.
            0
            Я кстати согласен с автором. Например, у нас есть код, много где завязанный на Hazelcast, у которого полно статических методов. По уму, надо рефакторить и выносить вызовы Hazelcast в класс-обертку, которую уже легко будет заменить моком. Но мы планируем от него избавиться в будущем, поэтому рефакторить ради тестов я смысла не вижу, если все равно этот код будет убран. Так что в данном конкретном случае проще заюзать PowerMock. Спасибо автору, буду теперь его использовать :)
        0
        автору спасибо, не знал про такую тулзу. Обычно тестировал методы, где есть статические вызовы путем выноса этих статических вызовов в отдельный метод и затем переопределяя его. Эта штука однозначно облегчит жизнь.

        Only users with full accounts can post comments. Log in, please.