Как работают сопоставители Mockito?

122

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

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

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

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

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

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

Джефф Боуман
источник

Ответы:

236

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

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

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

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

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

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

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

/* 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обертывает этот сопоставитель в стиле Hamcrest и возвращает, 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's different

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

Сопоставители возвращают фиктивные значения, такие как ноль, пустые коллекции или null. Mockito пытается вернуть безопасное и подходящее фиктивное значение, например 0 для anyInt()или any(Integer.class)или пустое значение List<String>для anyListOf(String.class). Из - за типа стирания, хотя, Mockito не хватает информации о типе , чтобы возвратить любое значение , но nullдля any()или argThat(...), что может привести к NullPointerException , если пытаться «авто-Unbox» а nullпримитивное значение.

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

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

Детали реализации

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

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

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

Это будет:

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

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

  • Мокито не может отличить 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().

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

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

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

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

  • InvalidUseOfMatchersException :

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

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

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

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

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

Устранение общих неисправностей

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

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

Джефф Боуман
источник
2
Спасибо за эту запись. Исключение NullPointerException с форматом when / thenReturn вызывало у меня проблемы, пока я не изменил его на doReturn / when.
yngwietiger
11

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

Если вызов метода соответствует более чем одному 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

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

tibtof
источник
2
Хотя это полезная информация, она касается заглушек, а не сопоставителей , поэтому этот вопрос может не иметь смысла. Порядок имеет значение, но только в том случае, если побеждает последняя определенная цепочка сопоставления : это означает, что сосуществующие заглушки часто объявляются наиболее специфичными или наименее специфичными, но в некоторых случаях вам может потребоваться очень широкое переопределение специально имитируемого поведения в одном тестовом примере. , после чего, возможно, потребуется дать более широкое определение.
Джефф Боуман
1
@JeffBowman Я подумал, что это имеет смысл по этому вопросу, поскольку вопрос касается сопоставлений mockito, и сопоставления могут использоваться при заглушке (как в большинстве ваших примеров). Поскольку поиск объяснения в Google привел меня к этому вопросу, я думаю, что здесь полезно иметь эту информацию.
tibtof