Pull to refresh

JMock и EasyMock: сравнение и howto в примерах и не только

Java *
Tutorial
Практически ни для кого не секрет, что при тестировании кода, использующего какие-то внешние компоненты, часто применяют подход mock-объектов. Для тех, кто всё же о нём не знает, кратко поясню: это такие объекты, которые имеют тот же интерфейс, что и используемые компоненты, но их поведение полностью задаётся в тесте, и их использование позволяет избежать поднятия полной инфраструктуры, необходимой приложению для запуска. Что ещё более важно, можно легко и непринуждённо проконтролировать, что код вызывал те или иные методы у mock-объекта с теми или иными аргументами.

В этой статье я проведу сравнительный анализ двух распространённых в Java библиотек для работы с mock'ами: EasyMock и JMock. Для осознания достаточно базового знания JUnit, а после прочтения этой статьи у вас будет весьма хорошее представление о том, как пользоваться обеими этими библиотеками.

Рассматриваемая задача

В качестве примера того, что необходимо оттестировать, будем рассматривать некоторое приложение, имеющее примерно такую структуру:
1
2
3
4
5
6
7
8
9
public class WayTooComplexClass {
    
    public WayTooComplexClass(String serverAddress) {/*...*/}
    
    public boolean save(long id, String data) {/*...*/}
    
    public String get(long id) {/*...*/}
    
}
Пусть реализация, скрытая троеточиями, в качестве хранилища использует какой-нибудь сервис, имеющий простое HTTP API (например, elliptics). С тем, чтобы для тестов постоянно где-то держать этот сервер, есть как минимум две проблемы:
  1. Необходимо, чтобы с каждой машины, на которой запускается тест, был доступ к этому серверу
  2. Описание поведения mock-сервера находится вне кода, и потому могут возникнуть различные неприятности, особенно если один разработчик обновит тест и в коде и на сервере, а другой не обновится, и тест, который вчера проходил, неожиданно сломается
Некоторые на этом моменте опускают руки, говорят, что их «код слишком сложен для Unit-тестов»™, и забивают их писать. К счастью, мы не из таких, и потому поднимем прямо из теста маленький HTTP-сервер, который будет отвечать на нужные запросы нужным образом. В этом примере я для таких целей использовал jetty. Такой код будет общим при использовании обеих mock-библиотек:

Mock-сервер для тестирования (можно не вчитываться)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class WayTooComplexClassTest {

    static interface RequestHandler {
        String handle(String target, String data);
    }   

    private static class MockHttpServerHandler extends AbstractHandler {

        private RequestHandler handler;

        public void setRequestHandler(RequestHandler handler) {
            this.handler = handler;
        }   

        @Override
        public void handle(String target, HttpServletRequest request,
                           HttpServletResponse response, int dispatch
                          ) throws IOException, ServletException {

            String req = IOUtils.toString(request.getInputStream(), "UTF-8");
            String result = handler.handle(target, req);
            response.setStatus(HttpStatus.ORDINAL_200_OK);
            response.setContentType("text/plain");

            final ServletOutputStream outputStream = response.getOutputStream();
            try {
                outputStream.print(result);
            } finally {
                outputStream.close();
            }   
        }   
    }   

    private static final MockHttpServerHandler SERVER_HANDLER = new MockHttpServerHandler();

    @BeforeClass
    public static void startServer() throws Exception {
        org.mortbay.jetty.Server server = new org.mortbay.jetty.Server();
        server.setHandler(SERVER_HANDLER);
        server.start();
    }   

    private final WayTooComplexClass wayTooComplex = 
              new WayTooComplexClass("http://localhost/9001");

    //Tests go here
}
Тут у нас есть три интересных момента. Первый — это строчки 3-5, описывающие интерфейс RequestHandler, который получает на вход цель запроса (например, в адресе http://habrahabr.ru/blogs/java/136466/ целью будет выделенный жирным /blogs/java/136466/) и данные, отправленные пользователем в теле запроса. Второй — строки с 7 по 32 — класс MockHttpServerHandler. Ему устанавливается RequestHandler, которому делегируется вся «бизнес-логика», а результат его работы записывается в HTTP-ответ. И третий — строки 36-41 — это метод startServer, который, как можно догадаться из аннотации и названия, вызывается перед тем, как начнут запускаться какие-либо тесты, перечисленные в этом классе, и запускает HTTP-сервер.

Первый и самый простой тест

Предположим, что в теории код, спрятанный в методе save, должен пройти по урлу {serverAddress}/upload/{id} и передать туда data. Проверим, происходит ли это в действительности.

JMock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Mockery context = new JUnit4Mockery();

@Test
public void testSaveWithJMock() {
    final long id = 13;
    final String data = "nanofilters";
    
    final RequestHandler requestHandler = context.mock(RequestHandler.class);

    context.checking(new Expectations() {{
        one(requestHandler).handle("/upload/" + id, data);
        will(returnValue("Saved " + id));
    }});

    SERVER_HANDLER.setRequestHandler(requestHandler);

    wayTooComplex.save(id, data);

    context.assertIsSatisfied();
}

В первой же строке нам необходимо создать JMock-контекст, в котором будет выполняться тест. Такого контекста достаточно одного на весь тест, но обойтись без него никак. Чтобы избежать проблем с несколькими mock-ами одного и того же, следует контекст создавать заново перед каждым тестом (то есть, внутри метода, помеченного аннотацией @Before) В восьмой строке мы легко и непринуждённо создаём mock для нашего интерфейса. Далее, в строках 10-13 мы описываем, какие вызовы должны произойти. Синтаксис на первый взгляд не очень интуитивно понятен, но со временем привыкаешь. В строке 11 мы указали, что ожидаем ровно один вызов метода handle с аргументами ("/upload/" + id) и (data). В строке 12 мы говорим, что последний вызов вернёт значение ("Saved " + id). Тут, как вы можете догадаться, нет типовой безопасности. Мы можем случайно передать туда значение не того типа и узнать об этом только в рантайме, схлопотав исключение. Зато если возвращаемое значение не важно, то можно этого вообще не писать: JMock автоматически вернёт значение по умолчанию (0, false, null или пустую строку). Далее мы говорим нашему мок-серверу, что нужно использовать свежесозданный мок-обработчик, вызываем у приложения тестируемый метод и в 19 строке проверяем, что все ожидаемые вызовы были сделаны. От последнего можно избавиться, добавив к самому тест-классу аннотацию @RunWith(JMock.class)

EasyMock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
IMocksControl control = EasyMock.createControl();

@Test
public void testSaveWithEasyMock() {
    final long id = 15;
    final String data = "cold fusion reactor";

    final RequestHandler requestHandler = control.createMock(RequestHandler.class);

    expect(requestHandler.handle("/upload/" + id, data)).andReturn("Saved " + id);
    control.replay();

    SERVER_HANDLER.setRequestHandler(requestHandler);

    wayTooComplex.save(id, data);

    control.verify();
}
В первой строчке мы создаём control, который является аналогом контекста JMock. Далее в восьмой строке создаём мок-объект, в десятой указываем, что ожидаем вызов метода handle с определёнными аргументами и говорим в таком случае возвращать определённое значение. Тут есть типовая безопасность: попытка передать в andReturn аргумент типа, отличного от String, приведёт к ошибке компиляции.
У EasyMock, с моей точки зрения, спецификация ожидаемого поведения более понятна. У такого подхода, правда, есть и недостаток: как вы можете видеть в строке 11, необходимо явно указывать, что запись ожидаемого поведения закончена. После всей «бизнес-логики» мы доходим до строки 17 и проверяем, что все ожидаемые методы были вызваны. Кстати, если нам плевать, что метод возвращает, то для void-методов можно опустить конструкцию expect и просто сделать вызов: requestHandler.handle("/upload/" + id, data). Кроме того, использовать control не обязательно, и можно просто сделать так:
1
2
3
4
5
final RequestHandler requestHandler = EasyMock.createMock(RequestHandler.class);
//...
EasyMock.replay(requestHandler);
//...
EasyMock.verify(requestHandler);


Более сложная реакция на вызов метода


Предположим теперь, что нам необходимо протестировать, что наше приложение ведёт себя корректно и при сбое внешнего компонента. Для этого достаточно организовать исключение в методе handle, и тогда jetty сам поставит http status 500.

JMock

1
2
3
4
5
6
7
8
@Test
public void testErrorHandlingWithJMock() {
    //...
    context.checking(new Expectations() {{
        one(requestHandler).handle("/upload/" + id, data);
        will(throwException(new RuntimeException("Somebody set up us the bomb.")));
    }});
}
Ничего сложного. Думаю, комментарии излишни. Можно разве что добавить, что из коробки will ещё умеет returnIterator, но можно устроить и что-нибудь собственное, реализовав интерфейс Action. Он, правда, не особо понятен, да и документация не ахти.

EasyMock

1
2
3
4
5
6
@Test
public void testErrorHandlingWithEasyMock() {
    //...
    expect(requestHandler.handle("/upload/" + id, data))
            .andThrow(new RuntimeException("All your base are belong to us."));
}
Тут тоже всё просто, но, как вы уже наверняка подозреваете, если метод ничего не возвращает (т.е. имеет тип void), приходится писать по-другому, и теряется общность стиля. Выглядело бы это так:
1
2
3
4
5
6
@Test
public void testErrorHandlingWithEasyMock() {
    //...
    requestHandler.handle("/upload/" + id, data); expectLastCall()
            .andThrow(new RuntimeException("You have no chance to survive make your time."));
}
Кроме andThrow можно ещё использовать andDelegateTo, который, как можно догадаться, делегирует вызов метода какому-то другому объекту (нет типовой безопасности во время компиляции!) и andAnswer. Для последнего нужно реализовать интерфейс IAnswer, и внутри метода answer написать любой код. Тут несколько меньше мощности, чем у JMock, но зато гораздо, гораздо больше удобства.

Matching аргументов при вызове метода


Теперь предположим, что мы точно не знаем, какие именно аргументы должны быть переданы в вызов mocked метода. Единственное, в чём мы точно уверены — это то, что аргумент target должен где-то содержать в себе id.

JMock

1
2
3
4
5
6
7
@Test
public void testArgumentMatchingWithJMock() {
    //...
    context.checking(new Expectations() {{
        one(requestHandler).handle(with(containsString(String.valueOf(id))), anything());
    }});
}
JMock использует matcher-ы от hamcrest и позволяет при необходимости добавить блекдж что-то нестандартное написать свой matcher.

EasyMock

1
2
3
4
5
6
@Test
public void testArgumentMatchingWithEasyMock() {
    //...
    expect(requestHandler.handle(contains(String.valueOf(id)), anyObject(String.class)))
            .andReturn(null);
}
Тут используются собственные matcher-ы, и потому набор уже готовых matcher-ов меньше. Однако то, что уже есть, сделано довольно прилично и удовлетворяет все основные потребности, а остальные точно так же, как и в JMock, можно удовлетворить, реализовав нужный matcher самостоятельно.

Число вызовов

Теперь немного поменяем условия, сказав, что какой-то метод должен вызваться несколько раз. Пример стал довольно далёким от реальности, но выдумывать более жизненный исключительно для того, чтобы показать что-то простое, не хочется.

JMock

1
2
3
4
5
6
7
@Test
public void testMultipleInvocationsWithJMock() {
    //...
    context.checking(new Expectations() {{
        between(2, 5).of(requestHandler).handle(anything(), anything());
    }});
}
Как мог догадаться внимательный читатель, во всех предыдущих примерах слово one было не просто так, а указывало на то, сколько вызовов ожидать. Поддерживаются все необходимые количества вызовов: будь то ноль или пофиг-сколько.

EasyMock

1
2
3
4
5
6
@Test
public void testMultipleInvocationsWithEasyMock() {
    //...
    expect(requestHandler.handle(anyObject(String.class), anyObject(String.class)))
            .andReturn(null).times(2, 5);
}
Тут тоже всё довольно просто, но есть небольшой минус: нет возможности сказать «как минимум n раз» без ограничения сверху. Это слегка странно, учитывая, что метод atLeastOnce есть. Чтобы ожидать как минимум три вызова, нужно написать примерно такой код:

1
2
3
4
5
6
7
@Test
public void testMultipleInvocationsWithEasyMock() {
    //...
    expect(requestHandler.handle(anyObject(String.class), anyObject(String.class)))
            .andReturn(null).times(3);
    expectLastCall().andReturn(null).anyTimes();
}


Заглушки


Нередко бывает так, что нам в принципе не очень важно, как и когда будут вызывать какой-то метод, а интересно лишь чтобы он существовал и что-нибудь возвращал.

JMock

1
2
3
4
5
6
7
8
@Test
public void testStubMethodsWithJMock() {
    //...
    context.checking(new Expectations() {{
        allowing(requestHandler).handle(anything(), anything());
        will(returnValue("There will be cake"));
    }});
}
Заметно, что это делается единообразно с определением числа раз. Вместо allowing можно сказать ignoring. Также при желании легко сделать весь Mock-объект одной большой заглушкой:
1
2
3
4
5
6
7
@Test
public void testStubObjectsWithJMock() {
    //...
    context.checking(new Expectations() {{
        allowing(requestHandler);
    }});
}

EasyMock

1
2
3
4
5
6
@Test
public void testStubMethodsWithEasyMock() {
    //...
    expect(requestHandler.handle(anyObject(String.class), anyObject(String.class)))
            .andStubReturn("Greetings, human.");
}
Слегка менее единообразно по стилю, но тоже вполне удобно. А вот сделать объект затычкой можно только при создании:
1
2
3
4
5
@Test
public void testStubObjectsWithEasyMock() {
    final RequestHandler requestHandler = createNiceMock(RequestHandler.class);
    //...
}
Методы тогда при вызове будут возвращать значение по умолчанию для своего типа (0, false или null). null будет возвращён в том числе и для String, тогда как в JMock для этого класса значение по умолчанию — пустая строка.

Проверка порядка вызова методов


Нередко оказывается важным то, в каком порядке вызываются методы. Предположим, что мы обзавелись паранойей, и сразу после загрузки файла решили скачивать его обратно и сверять с эталонным. Убедиться, что приложение это действительно делает (а то мало ли, не вызывает оно особого доверия...), можно так:

JMock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testStrictOrderingWithJMock() {
    //...
    final Sequence paranoia = context.sequence("shhh-they-are-watching-us");

    context.checking(new Expectations() {{
        one(requestHandler).handle("/upload/" + id, data);
        will(returnValue("Saved " + id));
        inSequence(paranoia);

        one(requestHandler).handle("/get/" + id, "");
        will(returnValue(data));
        inSequence(paranoia);
    }});
}
Тут нас интересуют строки 9 и 13, в которых мы, собственно, и проверяем порядок выполнения. Важно помнить, что inSequence будет следить за порядком только того вызова mock-объекта, непосредственно за которым он указан. Поэтому чтобы отследить, что десять разных вызовов будут сделаны в строгом порядке, придётся десять раз написать inSequence. Зато можно навешивать на вызов сразу несколько последовательностей.

EasyMock

1
2
3
4
5
6
7
@Test
public void testStrictOrderingWithEasyMock() {
    //...
    EasyMock.checkOrder(requestHandler, true);
    EasyMock.expect(requestHandler.handle("/upload/" + id, data)).andReturn("Saved " + id);
    EasyMock.expect(requestHandler.handle("/get/" + id, "")).andReturn(data);
}
Тут всё несколько проще: проверку можно включать и выключать по несколько раз для каждого мока по отдельности. Кроме того, строгим может быть целый control (см. первый пример), и тогда будет проверяться общий порядок для всех входящих в него mock-ов. Кроме того, мок или контрол можно сделать строгим сразу при создании, сказав createStrictMock. Конечно, такое количество зелёного нужно разбавить серьёзным минусом: один мок (или контрол) не может принимать участия в нескольких последовательностях. Тут даже понятия такого нет.

Условия, при которых вызов метода разрешён


В некоторых случаях необходимо промоделировать состояние приложения, которое будет изменяться при вызове некоторых методов, и проверяться при вызове некоторых других (или тех же). Пусть наше приложение может обратиться на один из двух инстансов elliptics (инстансы называются panola и yarbo) для загрузки файла, и потом должно скачать его с того же самого инстанса, на который загрузило. Это тоже можно проверить:

JMock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testStatesWithJMock() {
    //...
    final States progress = context.states("progress").startsAs("none");

    context.checking(new Expectations() {{
        one(requestHandler).handle("/panola/upload/" + id, data);
        will(returnValue("Saved " + id));
        when(progress.is("none")); then(progress.is("panola"));

        one(requestHandler).handle("/yarbo/upload/" + id, data);
        will(returnValue("Saved " + id));
        when(progress.is("none")); then(progress.is("yarbo"));

        one(requestHandler).handle("/panola/get/" + id, "");
        will(returnValue(data));
        when(progress.is("panola")); then(progress.is("done"));

        one(requestHandler).handle("/yarbo/get/" + id, "");
        will(returnValue(data));
        when(progress.is("yarbo")); then(progress.is("done"));
    }});
}
Смотреть нужно на строки 9, 13, 17 и 21. В каждой из них мы проверяем с помощью when, что приложение находится сейчас в правильном состоянии, и затем выставляем новое с помощью then. Очень удобно. Приверженцы парадигмы автоматного программирования, наверное, сейчас думают о том, что нашли способ тестирования их мечты.

EasyMock: аналога нет



Тестирование многопоточного кода


Вообще говоря, тестировать многопоточный код очень и очень сложно, потому что различных комбинаций того, когда тот или иной поток выполнит то или иное действие, очень много. Особенно если вы не объявляете всё критическими секциями, а стараетесь обойтись минимумом блокировок. Абстрагируемся сейчас от нашего замечательного приложения, и посмотрим прямо на то, как дела с поддержкой многопоточности у разных библиотек.

JMock


Честно сказать, для меня это было страшной мукой: документация по этому вопросу (как, впрочем, и по многим другим) довольно фиговая, и потому действовать приходилось методом проб и ошибок (а поскольку тогда я был в Амстердаме, и к тому же в аэропорте, это было не так-то и просто). В итоге меня спасли вручную скачанная версия JMock 2.6.0-RC2 с вручную же скачанной версией hamcrest-core 1.3.0RC1 и следующий код:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Synchroniser synchroniser = new Synchroniser();
private Mockery context = new JUnit4Mockery() {{
    this.setThreadingPolicy(synchroniser);
}};

@Test
public void testConcurrencyWithJMock() {
    //...
    final States progress = context.states("progress").startsAs("none");

    context.checking(new Expectations() {{
        //check something
        then(progress.is("done"));
    }});
    //do something in multiple threads
    
    synchroniser.waitUntil(states.is("done"), TimeUnit.SECONDS.toMillis(1));
}
Тут мы в первой строке создаём новый синхронизатор, в третьей говорим контексту, что его нужно использовать, а в 17й дожидаемся того, что все нужные операции были выполнены. То, что States позволяет это сделать — очень здорово, и в некоторых случаях избавляет от необходимости дожидаться того, когда приложение закончит свою работу.

EasyMock


Здесь есть полная поддержка многопоточности из коробки. Более того, её можно отключить, используя во время записи makeThreadSafe(mock, false), а при необходимости и проверить, что mock использовался из одного потока, сказав checkIsUsedInOneThread(mock, true)

Mocking классов


Иногда оказывается так, что разработчик модуля, который нужно в тесте заменить на mock, не потрудился сделать интерфейса, и у нас есть только конкретный класс. К счастью, если этот класс не final и не имеет final методов, то мы вполне можем сделать на него mock.

JMock

1
2
3
4
5
6
7
8
private Mockery context = new JUnit4Mockery() {{
    this.setImposteriser(ClassImposteriser.INSTANCE);
}};

@Test
public void testClassMockingWithJMock() {
    //...
}
Всё, что нужно сделать, это вызвать у контекста метод setImposteriser. В примере это происходит во второй строке.

EasyMock

1
2
3
4
5
6
import static org.easymock.classextension.EasyMock.*;

@Test
public void testClassMockingWithEasyMock() {
    //...
}
Тут достаточно использовать методы из другого класса, который, правда, находится в другом артефакте maven. Впрочем, с classextension вы можете спокойно создавать моки как на классы, так и на интерфейсы.

Частичный mocking


Ещё иногда может оказаться необходимым сделать mock только на часть методов класса, не трогая остальные.

JMock: такой возможности нет



EasyMock

1
2
3
4
5
6
7
@Test
public void testPartialMockingWithEasyMock() {
    //...
    IntArraySorter sorter = EasyMock.createMockBuilder(IntArraySorter.class)
        .addMockedMethod("sort", int[].class).createMock();
    //...
}
Всё просто и понятно. Также можно указывать, какие параметры передавать в какой конструктор. Все не-mocked методы будут делегированы настоящему классу.

Заключение

На этом примеры и сравнения закончены. Надеюсь, все, дочитавшие досюда, теперь неплохо умеют пользоваться и той и другой библиотекой, и в будущем, используя обретённые навыки, спасут себя от многих ошибок.

Возможно, читатель ждёт тут от меня ответа на вопрос «Так что же мне лучше использовать в своём проекте? JMock или EasyMock?». Ответ тут очень простой и однозначный: «Зависит от требований проекта. Каждая из этих библиотек — инструмент, и каждым из них нужно уметь пользоваться, а выбирать какой-то один нужно для конкретной задачи».

На этом всё. Жду интересных вопросов и замечаний!
Tags:
Hubs:
Total votes 26: ↑24 and ↓2 +22
Views 21K
Comments Comments 25