Как работают программы сопоставления Mockito?
Средства сопоставления аргументов Mockito (такие как any
, argThat
, eq
, same
ArgumentCaptor.capture()
, и....) ведут себя совсем не так, как средства сопоставления Hamcrest.
Средства сопоставления Mockito часто вызывают исключение InvalidUseOfMatchersException, даже в коде, который выполняется спустя долгое время после использования каких-либо средств сопоставления.
Средства сопоставления Mockito подчиняются странным правилам, таким как требование использования средств сопоставления Mockito только для всех аргументов, если один аргумент в данном методе использует средство сопоставления.
Программы сопоставления Mockito могут вызывать исключение NullPointerException при переопределении
Answer
s или при использовании(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
wrapArgumentMatcher<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]
Это позволит:
- Добавить
anyInt()
в стек. - Добавить
gt(10)
в стек. - Добавить
lt(20)
в стек. - Удалите
gt(10)
иlt(20)
и добавьтеand(gt(10), lt(20))
. - Вызов
foo.quux(0, 0)
, который (если не указано иное) возвращает значение по умолчаниюfalse
. Внутренне Mockito помечаетquux(int, int)
как самый последний вызов. - Вызов
when(false)
, который отбрасывает свой аргумент и готовится к методу stubquux(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
orverify
. Средства сопоставления никогда не должны использоваться в качестве заглушенных возвращаемых значений или полей / переменных.Убедитесь, что вы не вызываете макет как часть предоставления аргумента сопоставления.
Убедитесь, что вы не пытаетесь заглушить / проверить окончательный метод с помощью программы сопоставления. Это отличный способ оставить средство сопоставления в стеке, и если ваш метод 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
.