Перейти к основному содержимому

Руководство по ожиданиям JMockit

· 8 мин. чтения

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);
}};
}

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

2.2. Методы «с»

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 не используется для определения аргумента, для которого 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. Поле «Время»

Иногда мы хотим ограничить количество вызовов , ожидаемых для фиктивного метода. Для этого в 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; мы определили, что методForTimes3() будет вызываться от одного до трех раз .

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

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

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

В этом случае JMockit полагается на интерфейс Hamcrest Matcher . Вам просто нужно определить сопоставитель для конкретного сценария тестирования и использовать этот сопоставитель с вызовом 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. Возвращаемые значения

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

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» , exception и «bar» . Мы достигли этого, используя три разных назначения для поля результата .

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

Для methodReturnsInt() мы определили в строке 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. Статьи в серии

Все статьи цикла: