Mocking Liferay Services

Если вы пишете под Liferay, используете Service Builder и не знаете как протестировать свою логику, тогда этот пост для вас.

В сети можно найти различные варианты решения данной задачи. Многие из них направлены на специфическую конфигурацию зависимостей, которые позволят сервисам работать с реальной базой данных. Такие подходы сложны и противоречат принципам юнит тестирования. В этом случае надо либо поднимать базу в памяти и настраивать на ее использования сервиса, либо создавать заглушки, они же «моки», самих сервисов. Поэтому бизнес логику лучше вынести в отдельный слой.

Пока нет ничего сложного. С подходом определились, теперь необходимо найти инструмент, который позволит создавать заглушки на любой сгенерированный сервис. Такой инструмент есть — Mockito. В качестве льтернативы могу предложить Easymock, но примеры будут на Mockito.

Для примера возьмем сущность Status и опишем ее в service.xml:
<entity name="Status" table="status" local-service="true" remote-service="false">
    <column name="id" db-name="status_id" type="long" primary="true" id-type="increment"/>
    <column name="name" db-name="name" type="String"/>
</entity>

Соберем сервиса, после чего получим ряд классов, который все вместе образуют DAO слой для этой сущности. Обычно в бизнес логике используются классы *LocalServiceUtil, в нашем случае это класс StatusLocalServiceUtil. Допустим что у нас есть некий класс Controller, который вызывает метод getStatus класса StatusLocalServiceUtil и выполняет некую логику с результатом:
public class Controller {
    public void doAction(long statusId) throws SystemException, PortalException {
        // Получаем статус по его id
        Status status = StatusLocalServiceUtil.getStatus(statusId);
        // и выводим его в консоль
        System.out.println("Status = " + status);
    }
}

Целью является создание заглушки, которая будет эмулировать работу метода getStatus. А сложность в том, что этот метод статический и подменить его сложно, но возможно. Далее следует пример юнит теста, в котором происходит создание заглушки:
public class ControllerTest {
    private StatusLocalService service;
    private Controller controller;

    @BeforeMethod
    public void beforeMethod() {
        // Создаем мок сервиса используя библиотеку mockito
        service = mock(StatusLocalService.class);
        // После этой строчки класс StatusLocalServiceUtil 
        // будет использовать зуглушку или мок сервиса
        new StatusLocalServiceUtil().setService(service);
        // А это контроллер, который работает со
        // сгенерированными сервисами и ничего не знает
        // ни о каких моках
        controller = new Controller();
    }

    @Test
    public void testGetStatus()
            throws ServletException, SystemException, PortalException {
        // Конфигурируем сервис таким образом, что при вызове
        // метода с параметром 7 вернется уже другой мок —
        // мок объекта Status
        when(service.getStatus(eq(7L))).thenReturn(mock(Status.class));

        // Вот он самый, вызов метода, который тестируется
        controller.doAction(7);

        // А здесь проверяем был ли вызов метода getStatus
        // именно с параметром 7
        verify(service).getStatus(eq(7L));
    }
}

На самом деле дела обстоят сложнее и требуют более глубоких объяснений иерархии генерируемых сервисов, но для тестирования этого достаточно.