Как работают матчи Mockito?

Сопряжения аргументов Mockito (такие как any, argThat, eq, same и ArgumentCaptor.capture()) ведут себя очень по-другому от совпадений Hamcrest.

  • Соединители Mockito часто вызывают InvalidUseOfMatchersException, даже в коде, который выполняется задолго до того, как были использованы какие-либо матчи.

  • Соединители Mockito привязаны к странным правилам, например, только требуя использования сокетов Mockito для всех аргументов, если один аргумент в данном методе использует совпадение.

  • Соединители Mockito могут вызывать исключение NullPointerException при переопределении Answer или при использовании (Integer) any() и т.д.

  • Рефакторинг кода с макетами Mockito определенными способами может приводить к исключениям и неожиданному поведению и может полностью завершиться.

Почему такие макеты Mockito созданы так и как они реализованы?

Ответ 1

Mockito matchers являются статическими методами и вызовами тех методов, которые поддерживают аргументы во время вызовов when и verify.

Соединители Hamcrest (архивная версия) (или совпадения в стиле Hamcrest) являются объектами с объектами универсального назначения, которые реализуют Matcher<T> и раскрывают метод matches(T), который возвращает true, если объект соответствует критериям Matcher. Они предназначены для того, чтобы быть свободными от побочных эффектов и обычно используются в утверждениях, подобных приведенным ниже.

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Составы Mockito существуют, отдельно от шаблонов стиля Hamcrest, так что описания совпадающих выражений непосредственно вписываются в вызовы методов: Mockito matchers return T, где методы совпадения Hamcrest возвращают объекты Matcher ( типа Matcher<T>).

Соединители Mockito вызываются статическими методами, такими как eq, any, gt и startsWith на org.mockito.Matchers и org.mockito.AdditionalMatchers. Существуют также адаптеры, которые изменились в версиях Mockito:

  • Для Mockito 1.x, Matchers используются некоторые вызовы (например, intThat или argThat), которые являются макетами Mockito, которые непосредственно принимают параметры Hamcrest в качестве параметров. ArgumentMatcher<T> extended org.hamcrest.Matcher<T>, который использовался во внутреннем представлении Hamcrest и был базовым классом Matrix для Hamcrest вместо любого типа Mockito-сопряжения.
  • Для Mockito 2.0+ у Mockito больше нет прямой зависимости от Hamcrest. Matchers, выраженные как intThat или argThat wrap ArgumentMatcher<T> объекты, которые больше не реализуют org.hamcrest.Matcher<T>, но используются аналогичным образом, Адаптеры Hamcrest, такие как argThat и intThat, по-прежнему доступны, но вместо этого переместились в MockitoHamcrest.

Независимо от того, являются ли шаблоны Hamcrest или просто Hamcrest-стилем, они могут быть адаптированы так:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

В приведенном выше утверждении: foo.setPowerLevel - это метод, который принимает int. is(greaterThan(9000)) возвращает a Matcher<Integer>, который не будет работать как аргумент setPowerLevel. Соединитель Mockito intThat завершает совпадение в стиле Хамкреста и возвращает int, поэтому он может отображаться как аргумент; Сокеты Mockito, такие как gt(9000), переносят это целое выражение в один вызов, как в первой строке примера кода.

Какие сокеты делают/возвращают

when(foo.quux(3, 5)).thenReturn(true);

Когда не используются аргументы, Mockito записывает ваши значения аргументов и сравнивает их с их методами equals.

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one different

Когда вы вызываете соответствующий элемент типа any или gt (больше, чем), Mockito хранит объект-сопряжение, из-за которого Mockito пропускает проверку равенства и применяет ваш выбор. В случае argumentCaptor.capture() он сохраняет совпадение, которое сохраняет свой аргумент вместо этого для последующего контроля.

Матчи возвращают фиктивные значения, такие как нуль, пустые коллекции или null. Mockito пытается вернуть безопасное, соответствующее фиктивное значение, например 0 для anyInt() или any(Integer.class) или пустой List<String> для anyListOf(String.class). Однако из-за стирания стилей Mockito не имеет информации о типе, чтобы вернуть любое значение, но null для any() или argThat(...), что может вызвать исключение NullPointerException при попытке "auto-unbox" a null примитивного значения.

Подходы, такие как eq и gt, принимают значения параметров; в идеале эти значения должны вычисляться до начала старта/проверки. Вызов макета в середине издевательства над другим вызовом может помешать выполнению stubbing.

Методы сопоставления не могут использоваться как возвращаемые значения; например, нет способа фразы thenReturn(anyInt()) или thenReturn(any(Foo.class)) в Mockito. Mockito должен точно знать, какой экземпляр будет возвращаться при вызове stubbing, и не выберет для вас произвольное возвращаемое значение.

Сведения о реализации

Матчи хранятся (в виде совпадений объектов типа Hamcrest) в стеке, содержащемся в классе ArgumentMatcherStorage. MockitoCore и Matchers имеют собственный экземпляр ThreadSafeMockingProgress, который статически содержит экземпляры MockingProgress, поддерживающие ThreadLocal. Это MockingProgressImpl, в котором содержится конкретный ArgumentMatcherStorageImpl, Следовательно, состояние mock и matcher является статическим, но поточно-зависимым между классами Mockito и Matchers.

Большинство вызовов-ответчиков только добавляются в этот стек с исключением для таких элементов, как and, or и not. Это отлично соответствует (и полагается) порядку оценки Java, который вычисляет аргументы слева направо перед вызовом метода:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

Это будет:

  • Добавьте anyInt() в стек.
  • Добавьте gt(10) в стек.
  • Добавьте lt(20) в стек.
  • Удалите gt(10) и lt(20) и добавьте and(gt(10), lt(20)).
  • Вызов foo.quux(0, 0), который (если не оговорено иначе) возвращает значение по умолчанию false. Internally Mockito отмечает quux(int, int) как самый последний вызов.
  • Вызов when(false), который отбрасывает свой аргумент и готовит метод stub quux(int, int), идентифицированный в 5. Только два действительных состояния имеют длину стека 0 (равенство) или 2 (совпадения), и на стек (шаги 1 и 4), поэтому Mockito заглушает метод с помощью any() для его первого аргумента и and(gt(10), lt(20)) для своего второго аргумента и очищает стек.

Это демонстрирует несколько правил:

  • Mockito не может определить разницу между quux(anyInt(), 0) и quux(0, anyInt()). Оба они выглядят как вызов quux(0, 0) с одним совпадением int в стеке. Следовательно, если вы используете один матчи, вы должны сопоставить все аргументы.

  • Заказ звонков не просто важен, это то, что заставляет все это работать. Выделение шаблонов для переменных обычно не работает, поскольку обычно он изменяет порядок вызова. Тем не менее, извлечение сокетов к методам отлично работает.

    int between10And20 = and(gt(10), lt(20));
    /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
    // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
    
    public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
    /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
    // The helper method calls the matcher methods in the right order.
    
  • Стек довольно часто меняется, и Mockito не может его тщательно обработать. Он может проверять только стек, когда вы взаимодействуете с Mockito или макет, и должен принимать согласные, не зная, используются ли они немедленно или случайно удалены. Теоретически, стек всегда должен быть пустым вне вызова when или verify, но Mockito не может проверить это автоматически. Вы можете проверить вручную с помощью Mockito.validateMockitoUsage().

  • При вызове when Mockito на самом деле вызывает метод, о котором идет речь, что вызовет исключение, если вы запустили метод для исключения исключения (или потребовали ненулевые или ненулевые значения), doReturn и doAnswer (и т.д.) не ссылаются на фактический метод и часто являются полезной альтернативой.

  • Если вы вызвали метод mock в середине stubbing (например, для вычисления ответа для eq -сервера), Mockito проверил бы длину стека на этот вызов и, скорее всего, не получится.

  • Если вы попытаетесь сделать что-то плохое, например stubbing/verifying final method, Mockito вызовет реальный метод, а также уйдет дополнительные сокеты в стеке. Вызов метода final не может генерировать исключение, но вы можете получить InvalidUseOfMatchersException от бродячих совпадений, когда вы будете затем взаимодействовать с макетом.

Общие проблемы

  • InvalidUseOfMatchersException:

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

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

    • Убедитесь, что вы не пытаетесь заглушить/проверить окончательный метод с помощью соединителя. Это отличный способ оставить помощника в стеке, и если ваш последний метод не сделает исключение, это может быть единственный раз, когда вы поймете, что метод, который вы издеваетесь, является окончательным.

  • NullPointerException с примитивными аргументами: (Integer) any() возвращает значение null, а any(Integer.class) возвращает 0; это может привести к NullPointerException, если вы ожидаете int вместо Integer. В любом случае, предпочитайте anyInt(), который будет возвращать ноль, а также пропустить шаг автоматического бокса.

  • Исключение NullPointerException или другие исключения: Вызовы when(foo.bar(any())).thenReturn(baz) на самом деле вызовут foo.bar(null), который, возможно, запустил исключение при получении нулевого аргумента. Переключение на doReturn(baz).when(foo).bar(any()) пропускает зашитое поведение.

Общее устранение неполадок

  • Используйте MockitoJUnitRunner или явно вызывайте validateMockitoUsage в вашем методе tearDown или @After (который бегун сделает для вас автоматически). Это поможет определить, были ли вы неправильно использованы сокеты.

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

Ответ 2

Просто небольшое дополнение к Джеффу Боумену отличный ответ, так как я нашел этот вопрос при поиске решения одной из моих собственных проблем:

Если вызов метода соответствует нескольким обученным вызовам when, порядок вызовов when важен и должен быть от более широкого до наиболее конкретного. Начиная с одного из примеров Джеффа:

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

- это порядок, который обеспечивает (возможно) желаемый результат:

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false

Если вы меняете обратные вызовы, тогда результат всегда будет true.