Вопрос-ответ

How do Mockito matchers work?

Как работают программы сопоставления Mockito?

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


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


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


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


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


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

Переведено автоматически
Ответ 1

Средства сопоставления 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 вызываются с помощью статических методов, таких как 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 matcher вместо какого-либо Mockito matcher.

  • Для 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)) возвращает Matcher<Integer>, который не будет работать в качестве setPowerLevel аргумента. Средство сопоставления Mockito intThat оборачивает это средство сопоставления в стиле Hamcrest и возвращает an, 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 for any() или argThat(...), что может вызвать исключение NullPointerException при попытке "автоматической распаковки" null примитивного значения.

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

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

Подробности реализации

Средства сопоставления хранятся (как средства сопоставления объектов в стиле Hamcrest) в стеке, содержащемся в классе с именем ArgumentMatcherStorage. У каждого MockitoCore и средств сопоставления есть экземпляр ThreadSafeMockingProgress, который статически содержит ThreadLocal, содержащий экземпляры MockingProgress. Именно этот 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]

Это позволит:


  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), который отбрасывает свой аргумент и готовится к методу 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 (etc) не вызывают сам метод и часто являются полезной альтернативой.



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



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



Распространенные проблемы


  • Исключение InvalidUseOfMatchersException:



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



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



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





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



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



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


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



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



Ответ 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

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

java