¿Cómo funcionan los comparadores de Mockito?
Los comparadores de argumentos Mockito (como any
, argThat
, eq
, same
y ArgumentCaptor.capture()
) se comportan de manera muy diferente a los comparadores de Hamcrest.
Los comparadores de Mockito frecuentemente causan InvalidUseOfMatchersException, incluso en código que se ejecuta mucho después de que se usaron los comparadores.
Los comparadores de Mockito están sujetos a reglas extrañas, como requerir solo el uso de comparadores de Mockito para todos los argumentos si un argumento en un método determinado usa un comparador.
Los comparadores de Mockito pueden causar NullPointerException al anular
Answer
s o al usar(Integer) any()
etc.Refactorizar el código con comparadores Mockito de ciertas maneras puede producir excepciones y comportamientos inesperados, y puede fallar por completo.
¿Por qué los comparadores Mockito están diseñados así y cómo se implementan?
Los comparadores de Mockito son métodos estáticos y llamadas a esos métodos, que sustituyen a los argumentos durante las llamadas a when
y verify
.
Los comparadores Hamcrest (versión archivada) (o comparadores estilo Hamcrest) son instancias de objetos sin estado y de propósito general que implementan Matcher<T>
y exponen un método matches(T)
que devuelve verdadero si el objeto coincide con los criterios del Matcher. Están destinados a estar libres de efectos secundarios y generalmente se utilizan en afirmaciones como la que se muestra a continuación.
/* Mockito */ verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));
Existen comparadores Mockito, separados de los comparadores estilo Hamcrest, de modo que las descripciones de las expresiones coincidentes encajan directamente en las invocaciones de métodos : los comparadores Mockito regresan T
donde los métodos comparadores Hamcrest devuelven objetos Matcher (de tipo Matcher<T>
).
Los comparadores de Mockito se invocan mediante métodos estáticos como eq
, any
, gt
y startsWith
on org.mockito.Matchers
and org.mockito.AdditionalMatchers
. También hay adaptadores que han cambiado en las versiones de Mockito:
- Para Mockito 1.x,
Matchers
algunas llamadas destacadas (comointThat
oargThat
) son comparadores de Mockito que aceptan directamente comparadores de Hamcrest como parámetros.ArgumentMatcher<T>
extendidoorg.hamcrest.Matcher<T>
, que se usó en la representación interna de Hamcrest y era una clase base de comparación de Hamcrest en lugar de cualquier tipo de comparación de Mockito. - Para Mockito 2.0+, Mockito ya no depende directamente de Hamcrest.
Matchers
llamadas redactadasintThat
oargThat
envueltas comoArgumentMatcher<T>
objetos que ya no se implementanorg.hamcrest.Matcher<T>
pero que se usan de manera similar. Los adaptadores Hamcrest comoargThat
yintThat
todavía están disponibles, pero se han mudado aMockitoHamcrest
su lugar.
Independientemente de si los emparejadores son Hamcrest o simplemente estilo Hamcrest, se pueden adaptar así:
/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));
En la declaración anterior: foo.setPowerLevel
es un método que acepta un int
. is(greaterThan(9000))
devuelve a Matcher<Integer>
, que no funcionaría como setPowerLevel
argumento. El comparador Mockito intThat
envuelve ese Matcher estilo Hamcrest y devuelve un int
para que pueda aparecer como un argumento; A los emparejadores de Mockito les gustagt(9000)
gusta envolver esa expresión completa en una sola llamada, como en la primera línea del código de ejemplo.
¿Qué hacen/regresan los emparejadores?
when(foo.quux(3, 5)).thenReturn(true);
Cuando no utiliza comparadores de argumentos, Mockito registra los valores de sus argumentos y los compara con sus equals
métodos.
when(foo.quux(eq(3), eq(5))).thenReturn(true); // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different
Cuando llamas a un comparador como any
o gt
(mayor que), Mockito almacena un objeto comparador que hace que Mockito omita esa verificación de igualdad y aplique la coincidencia de tu elección. En el caso deargumentCaptor.capture()
esto, almacena un comparador que guarda su argumento para una inspección posterior.
Los comparadores devuelven valores ficticios como cero, colecciones vacías o null
. Mockito intenta devolver un valor ficticio apropiado y seguro, como 0 para anyInt()
o any(Integer.class)
o vacío List<String>
para anyListOf(String.class)
. Sin embargo, debido al borrado de tipos, Mockito carece de información de tipo para devolver cualquier valor que null
no any()
sea o argThat(...)
, lo que puede causar una excepción NullPointerException si se intenta "descomprimir automáticamente" unnull
valor primitivo.
A los emparejadores les gusta eq
ygt
toman valores de parámetros; Lo ideal es que estos valores se calculen antes de que comience la verificación/stubping. Llamar a un simulacro en medio de burlarse de otro llamado puede interferir con el stubbing.
Los métodos de comparación no se pueden utilizar como valores de retorno; no hay manera de expresar thenReturn(anyInt())
othenReturn(any(Foo.class))
en Mockito, por ejemplo. Mockito necesita saber exactamente qué instancia devolver en las llamadas de código auxiliar y no elegirá un valor de retorno arbitrario por usted.
Detalles de implementacion
Los comparadores se almacenan (como comparadores de objetos estilo Hamcrest) en una pila contenida en una clase llamada ArgumentMatcherStorage . MockitoCore y Matchers poseen cada uno una instancia de ThreadSafeMockingProgress , que contiene estáticamente un ThreadLocal que contiene instancias de MockingProgress. Es este MockingProgressImpl el que contiene un ArgumentMatcherStorageImpl concreto. concreto . En consecuencia, el estado de simulación y comparación es estático pero tiene un alcance consistente entre las clases Mockito y Matchers.
La mayoría de las llamadas de comparación solo se agregan a esta pila, con una excepción para coincidencias como and
, or
ynot
. Esto corresponde perfectamente (y depende de) el orden de evaluación de Java , que evalúa los argumentos de izquierda a derecha antes de invocar un método:
when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6] [5] [1] [4] [2] [3]
Esta voluntad:
- Añadir
anyInt()
a la pila. - Añadir
gt(10)
a la pila. - Añadir
lt(20)
a la pila. - Retirar
gt(10)
ylt(20)
agregarand(gt(10), lt(20))
. - Call
foo.quux(0, 0)
, que (a menos que se indique lo contrario) devuelve el valor predeterminadofalse
. Marcas internas de Mockitoquux(int, int)
como la llamada más reciente. - Call
when(false)
, que descarta su argumento y se prepara para el método stubquux(int, int)
identificado en 5. Los únicos dos estados válidos son con longitud de pila 0 (igualdad) o 2 (coincidencias), y hay dos coincidencias en la pila (pasos 1 y 4), por lo que Mockito bloquea el método con unany()
comparador para su primer argumento yand(gt(10), lt(20))
para su segundo argumento y limpia la pila.
Esto demuestra algunas reglas:
Mockito no puede distinguir entre
quux(anyInt(), 0)
yquux(0, anyInt())
. Ambos parecen una llamada aquux(0, 0)
con un int matcher en la pila. En consecuencia, si utiliza un comparador, debe hacer coincidir todos los argumentos.El orden de las llamadas no sólo es importante, es lo que hace que todo funcione . Extraer coincidencias a variables generalmente no funciona porque generalmente cambia el orden de las llamadas. Sin embargo, extraer coincidencias de métodos funciona muy bien.
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.
La pila cambia con tanta frecuencia que Mockito no puede controlarla con mucho cuidado. Solo puede verificar la pila cuando interactúas con Mockito o un simulacro, y tiene que aceptar coincidencias sin saber si se usan inmediatamente o se abandonan accidentalmente. En teoría, la pila siempre debería estar vacía fuera de una llamada a
when
overify
, pero Mockito no puede comprobarlo automáticamente. Puedes comprobarlo manualmente conMockito.validateMockitoUsage()
.En una llamada a
when
, Mockito en realidad llama al método en cuestión, lo que generará una excepción si ha bloqueado el método para generar una excepción (o requiere valores distintos de cero o no nulos).doReturn
ydoAnswer
(etc.) no invocan el método real y suelen ser una alternativa útil.Si hubiera llamado a un método simulado en medio de una prueba (por ejemplo, para calcular una respuesta para un
eq
comparador), Mockito verificaría la longitud de la pila con esa llamada y probablemente fallaría.Si intentas hacer algo malo, como tropezar/verificar un método final , Mockito llamará al método real y también dejará coincidencias adicionales en la pila . Es posible que la
final
llamada al método no genere una excepción, pero es posible que obtenga una InvalidUseOfMatchersException de los comparadores callejeros la próxima vez que interactúe con un simulacro.
Problemas comunes
InvalidUseOfMatchersException :
Verifique que cada argumento tenga exactamente una llamada de comparación, si es que usa coincidencias, y que no haya usado una comparación fuera de una llamada
when
o .verify
Las coincidencias nunca deben usarse como valores de retorno o campos/variables.Verifique que no esté llamando a un simulacro como parte de proporcionar un argumento de comparación.
Verifique que no esté intentando detectar/verificar un método final con un comparador. Es una excelente manera de dejar un comparador en la pila y, a menos que su método final arroje una excepción, esta podría ser la única vez que se dé cuenta de que el método del que se está burlando es definitivo.
NullPointerException con argumentos primitivos:
(Integer) any()
devuelve nulo mientrasany(Integer.class)
devuelve 0; esto puede causar unNullPointerException
si espera unint
en lugar de un número entero. En cualquier caso, prefieraanyInt()
, que devolverá cero y también omitirá el paso de boxeo automático.NullPointerException u otras excepciones: las llamadas a
when(foo.bar(any())).thenReturn(baz)
en realidad llamaránfoo.bar(null)
a , lo que podrías haber bloqueado para generar una excepción al recibir un argumento nulo. Cambiar adoReturn(baz).when(foo).bar(any())
omite el comportamiento de trozos .
Solución de problemas generales
Utilice MockitoJUnitRunner , o llame explícitamente
validateMockitoUsage
a su métodotearDown
o@After
(lo que el corredor haría por usted automáticamente). Esto ayudará a determinar si has hecho un mal uso de los comparadores.Para fines de depuración, agregue llamadas a
validateMockitoUsage
su código directamente. Esto arrojará si tiene algo en la pila, lo cual es una buena advertencia de un mal síntoma.
Sólo una pequeña adición a la excelente respuesta de Jeff Bowman, ya que encontré esta pregunta mientras buscaba una solución a uno de mis propios problemas:
Si una llamada a un método coincide con más de las when
llamadas entrenadas de un simulacro, el orden de las when
llamadas es importante y debe ser del más amplio al más específico. A partir de uno de los ejemplos de Jeff:
when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);
es el orden que asegura el resultado (probablemente) deseado:
foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false
Si inviertes las llamadas cuando, el resultado siempre será true
.