«1. Введение

Эта статья является второй частью серии JMockit. Вы можете прочитать первую статью, так как мы предполагаем, что вы уже знакомы с основами JMockit.

Сегодня мы углубимся и сосредоточимся на ожиданиях. Мы покажем, как определить более конкретное или общее сопоставление аргументов и более продвинутые способы определения значений.

2. Сопоставление значений аргументов

Следующие подходы применимы как к ожиданиям, так и к проверкам.

2.1. «Любые» поля

JMockit предлагает набор служебных полей для более универсального сопоставления аргументов. Одной из таких утилит являются поля anyX.

Они будут проверять, было ли передано любое значение, и есть по одному для каждого примитивного типа (и соответствующего класса-оболочки), одно для строк и «универсальное» для типа Object.

Рассмотрим пример:

public interface ExpectationsCollaborator {
    String methodForAny1(String s, int i, Boolean b);
    void methodForAny2(Long l, List<String> lst);
}

@Test
public void test(@Mocked ExpectationsCollaborator mock) throws Exception {
    new Expectations() {{
        mock.methodForAny1(anyString, anyInt, anyBoolean); 
        result = "any";
    }};

    Assert.assertEquals("any", mock.methodForAny1("barfooxyz", 0, Boolean.FALSE));
    mock.methodForAny2(2L, new ArrayList<>());

    new FullVerifications() {{
        mock.methodForAny2(anyLong, (List<String>) any);
    }};
}

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

2.2. Методы «With»

JMockit также предоставляет несколько методов, помогающих с сопоставлением общих аргументов. Это методы withX.

Они допускают более сложное сопоставление, чем поля anyX. Здесь мы можем увидеть пример, в котором мы определим ожидание для метода, который будет запускаться со строкой, содержащей foo, целое число, не равное 1, ненулевое логическое значение и любой экземпляр класса List:

public interface ExpectationsCollaborator {
    String methodForWith1(String s, int i);
    void methodForWith2(Boolean b, List<String> l);
}

@Test
public void testForWith(@Mocked ExpectationsCollaborator mock) throws Exception {
    new Expectations() {{
        mock.methodForWith1(withSubstring("foo"), withNotEqual(1));
        result = "with";
    }};

    assertEquals("with", mock.methodForWith1("barfooxyz", 2));
    mock.methodForWith2(Boolean.TRUE, new ArrayList<>());

    new Verifications() {{
        mock.methodForWith2(withNotNull(), withInstanceOf(List.class));
    }};
}

Вы можете увидеть полный список методов withX в документации JMockit.

Учтите, что специальные функции with(Delegate) и withArgThat(Matcher) будут рассмотрены в отдельном подразделе.

2.3. Null Is Not Null

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

На самом деле null используется как синтаксический сахар, чтобы определить, что будет передан любой объект (поэтому его можно использовать только для параметров ссылочного типа). Чтобы конкретно убедиться, что данный параметр получает нулевую ссылку, можно использовать сопоставитель withNull().

В следующем примере мы определим поведение макета, который должен запускаться, когда переданы следующие аргументы: любая строка, любой список и пустая ссылка:

public interface ExpectationsCollaborator {
    String methodForNulls1(String s, List<String> l);
    void methodForNulls2(String s, List<String> l);
}

@Test
public void testWithNulls(@Mocked ExpectationsCollaborator mock){
    new Expectations() {{
        mock.methodForNulls1(anyString, null); 
        result = "null";
    }};
    
    assertEquals("null", mock.methodForNulls1("blablabla", new ArrayList<String>()));
    mock.methodForNulls2("blablabla", null);
    
    new Verifications() {{
        mock.methodForNulls2(anyString, (List<String>) withNull());
    }};
}

Обратите внимание на разницу: null означает любой список, а withNull() означает нулевую ссылку на список. В частности, это избавляет от необходимости приводить значение к объявленному типу параметра (видите, что нужно было привести третий аргумент, а не второй).

Единственным условием для использования этого является то, что для ожидания использовался хотя бы один явный сопоставитель аргументов (либо метод with, либо любое поле).

2.4. Поле «Times»

Иногда нам нужно ограничить количество вызовов, ожидаемых для фиктивного метода. Для этого в JMockit есть зарезервированные слова times, minTimes и maxTimes (все три допускают только неотрицательные целые числа).

public interface ExpectationsCollaborator {
    void methodForTimes1();
    void methodForTimes2();
    void methodForTimes3();
}

@Test
public void testWithTimes(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{
        mock.methodForTimes1(); times = 2;
        mock.methodForTimes2();
    }};
    
    mock.methodForTimes1();
    mock.methodForTimes1();
    mock.methodForTimes2();
    mock.methodForTimes3();
    mock.methodForTimes3();
    mock.methodForTimes3();
    
    new Verifications() {{
        mock.methodForTimes3(); minTimes = 1; maxTimes = 3;
    }};
}

В этом примере мы определили, что ровно два вызова (не один, не три, ровно два) методаForTimes1() должны быть выполнены с использованием строки times = 2;.

Затем мы использовали поведение по умолчанию (если не задано ограничение повторения, используется minTimes = 1;), чтобы определить, что по крайней мере один вызов будет выполнен для methodForTimes2().

Наконец, использование minTimes = 1; затем maxTimes = 3; мы определили, что вызов methodForTimes3() будет происходить от одного до трех раз.

Учтите, что и minTimes, и maxTimes могут быть указаны для одного и того же ожидания, если minTimes назначается первым. С другой стороны, время можно использовать только отдельно.

2.5. Пользовательское сопоставление аргументов

Иногда сопоставление аргументов не является таким прямым, как простое указание значения или использование некоторых предопределенных утилит (anyX или withX).

В таких случаях JMockit использует интерфейс Matcher от Hamcrest. Вам просто нужно определить сопоставитель для конкретного сценария тестирования и использовать этот сопоставитель с вызовом withArgThat().

Давайте рассмотрим пример сопоставления определенного класса с переданным объектом:

public interface ExpectationsCollaborator {
    void methodForArgThat(Object o);
}

public class Model {
    public String getInfo(){
        return "info";
    }
}

@Test
public void testCustomArgumentMatching(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{
        mock.methodForArgThat(withArgThat(new BaseMatcher<Object>() {
            @Override
            public boolean matches(Object item) {
                return item instanceof Model && "info".equals(((Model) item).getInfo());
            }

            @Override
            public void describeTo(Description description) { }
        }));
    }};
    mock.methodForArgThat(new Model());
}

«

«3. Возвращаемые значения

Давайте теперь посмотрим на возвращаемые значения; имейте в виду, что следующие подходы применимы только к ожиданиям, поскольку для проверок нельзя определить возвращаемые значения.

3.1. Результат и возврат (€¦)

При использовании JMockit у вас есть три разных способа определения ожидаемого результата вызова фиктивного метода. Из всех трех мы сейчас поговорим о первых двух (самых простых), которые наверняка покроют 90% случаев повседневного использования.

    Это поле результата и метод return(Object…):

С полем результата вы можете определить одно возвращаемое значение для любого непустого возвращающего фиктивного метода. Это возвращаемое значение также может быть выброшенным исключением (на этот раз работает как для методов возврата non-void, так и для методов возврата void). Несколько назначений полей результатов могут быть выполнены, чтобы возвращать более одного значения для более чем одного вызова метода (вы можете смешивать как возвращаемые значения, так и выдаваемые ошибки). Такое же поведение будет достигнуто при присвоении результату списка или массива значений (того же типа, что и возвращаемый тип издеваемого метода, здесь НЕТ исключений). Метод return(Object…) — это синтаксический сахар для возврата нескольких значений в одно и то же время.

public interface ExpectationsCollaborator{
    String methodReturnsString();
    int methodReturnsInt();
}

@Test
public void testResultAndReturns(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{
        mock.methodReturnsString();
        result = "foo";
        result = new Exception();
        result = "bar";
        returns("foo", "bar");
        mock.methodReturnsInt();
        result = new int[]{1, 2, 3};
        result = 1;
    }};

    assertEquals("Should return foo", "foo", mock.methodReturnsString());
    try {
        mock.methodReturnsString();
        fail("Shouldn't reach here");
    } catch (Exception e) {
        // NOOP
    }
    assertEquals("Should return bar", "bar", mock.methodReturnsString());
    assertEquals("Should return 1", 1, mock.methodReturnsInt());
    assertEquals("Should return 2", 2, mock.methodReturnsInt());
    assertEquals("Should return 3", 3, mock.methodReturnsInt());
    assertEquals("Should return foo", "foo", mock.methodReturnsString());
    assertEquals("Should return bar", "bar", mock.methodReturnsString());
    assertEquals("Should return 1", 1, mock.methodReturnsInt());
}

Это легче показать с помощью фрагмента кода:

В этом примере мы определили, что для первых трех вызовов methodReturnsString() ожидаемые возвраты (по порядку) «foo» , исключение и «бар». Мы достигли этого, используя три разных назначения для поля результата.

Затем в строке 14 мы определили, что для четвертого и пятого вызовов «foo» и «bar» должны быть возвращены с помощью метода return(Object…).

Для методаReturnsInt() мы определили в строке 13, чтобы возвращать 1, 2 и, наконец, 3, назначая массив с различными результатами в поле результата, а в строке 15 мы определили, чтобы возвращать 1 простым присвоением в поле результата .

Как видите, существует несколько способов определения возвращаемых значений для имитируемых методов.

3.2. Делегаторы

В завершение статьи мы рассмотрим третий способ определения возвращаемого значения: интерфейс Delegate. Этот интерфейс используется для определения более сложных возвращаемых значений при определении фиктивных методов.

public interface ExpectationsCollaborator {
    int methodForDelegate(int i);
}

@Test
public void testDelegate(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{
        mock.methodForDelegate(anyInt);
            
        result = new Delegate() {
            int delegate(int i) throws Exception {
                if (i < 3) {
                    return 5;
                } else {
                    throw new Exception();
                }
            }
        };
    }};

    assertEquals("Should return 5", 5, mock.methodForDelegate(1));
    try {
        mock.methodForDelegate(3);
        fail("Shouldn't reach here");
    } catch (Exception e) {
    }
}

Мы рассмотрим пример для простого объяснения:

Способ использования делегатора заключается в создании для него нового экземпляра и назначении его в поле возврата. В этом новом экземпляре вы должны создать новый метод с теми же параметрами и типом возвращаемого значения, что и у фиктивного метода (вы можете использовать для него любое имя). Внутри этого нового метода используйте любую реализацию, которую вы хотите, чтобы вернуть желаемое значение.

В примере мы сделали реализацию, в которой 5 должно быть возвращено, когда значение, переданное фиктивному методу, меньше 3, а в противном случае выдается исключение (обратите внимание, что мы должны были использовать times = 2; так что второй вызов ожидается, так как мы потеряли поведение по умолчанию, определив возвращаемое значение).

Может показаться, что кода много, но в некоторых случаях это единственный способ добиться желаемого результата.

4. Заключение

Этим мы практически показали все, что нам нужно для создания ожиданий и проверок для наших повседневных тестов.

Мы, конечно же, опубликуем больше статей на JMockit, так что следите за обновлениями, чтобы узнать больше.

И, как всегда, полную реализацию этого руководства можно найти в проекте GitHub.

4.1. Статьи серии

    Все статьи серии: