¿Cómo funcionan los emparejadores Mockito?

122

Comparadores de argumentos Mockito (tales como any, argThat, eq, same, y ArgumentCaptor.capture()) se comportan de manera muy diferente de comparadores Hamcrest.

  • Los emparejadores de Mockito con frecuencia causan InvalidUseOfMatchersException, incluso en el código que se ejecuta mucho después de que se hayan usado los emparejadores.

  • Los emparejadores Mockito están sujetos a reglas extrañas, como que solo requieren el uso de emparejadores Mockito para todos los argumentos si un argumento en un método dado usa un emparejador.

  • Los emparejadores de Mockito pueden causar NullPointerException al anular so Answercuando se usan, (Integer) any()etc.

  • La refactorización del código con los comparadores de Mockito de determinadas formas puede producir excepciones y comportamientos inesperados, y puede fallar por completo.

¿Por qué los comparadores de Mockito se diseñan así y cómo se implementan?

Jeff Bowman
fuente

Respuestas:

236

Los comparadores de Mockito son métodos estáticos y llamadas a esos métodos, que sustituyen a los argumentos durante las llamadas a wheny verify.

Los emparejadores de Hamcrest (versión archivada) (o los emparejadores de estilo Hamcrest) son instancias de objetos de propósito general sin estado 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 usan en afirmaciones como la siguiente.

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

Los emparejadores Mockito existen, separados de los emparejadores estilo Hamcrest, de modo que las descripciones de las expresiones coincidentes encajan directamente en las invocaciones de métodos : Los emparejadores Mockito devuelven Tdonde los métodos de emparejamiento Hamcrest devuelven objetos Matcher (de tipo Matcher<T>).

Igualadores de Mockito se invocan a través de los métodos estáticos, tales como eq, any, gt, y startsWithen org.mockito.Matchersy org.mockito.AdditionalMatchers. También hay adaptadores, que han cambiado en las versiones de Mockito:

  • Para Mockito 1.x, Matchersalgunas llamadas (como intThato argThat) son comparadores de Mockito que aceptan directamente los comparadores de Hamcrest como parámetros. ArgumentMatcher<T>extendido org.hamcrest.Matcher<T>, que se usó en la representación interna de Hamcrest y era una clase base de emparejamiento de Hamcrest en lugar de cualquier tipo de emparejador de Mockito.
  • Para Mockito 2.0+, Mockito ya no tiene una dependencia directa de Hamcrest. Matchersllamadas formuladas como intThato argThatenvuelven ArgumentMatcher<T>objetos que ya no se implementan org.hamcrest.Matcher<T>pero que se usan de manera similar. Los adaptadores Hamcrest como argThaty intThattodavía están disponibles, pero se han trasladado a MockitoHamcrest.

Independientemente de si los matchers 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.setPowerLeveles un método que acepta un int. is(greaterThan(9000))devuelve un Matcher<Integer>, que no funcionaría como setPowerLevelargumento. El Matcher Mockito intThatenvuelve ese Matcher estilo Hamcrest y devuelve un intpara que pueda aparecer como un argumento; Los comparadores de Mockito gt(9000)envolverían toda esa expresión en una sola llamada, como en la primera línea del código de ejemplo.

¿Qué hacen los emparejadores / regresan?

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 equalsmé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 llama a un emparejador como anyo gt(mayor que), Mockito almacena un objeto de emparejamiento que hace que Mockito omita esa verificación de igualdad y aplique su coincidencia de elección. En el caso de argumentCaptor.capture()que 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 seguro, apropiado, como por 0 anyInt()o any(Integer.class)o un vacío List<String>para anyListOf(String.class). Sin embargo, debido al borrado de tipo, Mockito carece de información de tipo para devolver cualquier valor excepto nullpara any()o argThat(...), lo que puede causar una NullPointerException si intenta "desempaquetar automáticamente" un nullvalor primitivo.

A los comparadores les gusta eqy gttoman valores de parámetros; idealmente, estos valores deben calcularse antes de que comience la verificación / apéndice. Hacer un simulacro en medio de una burla de otra llamada puede interferir con la respuesta.

Los métodos de comparación no se pueden utilizar como valores de retorno; no hay forma de frasear thenReturn(anyInt())ni thenReturn(any(Foo.class))en Mockito, por ejemplo. Mockito necesita saber exactamente qué instancia devolver en llamadas de stubbing y no elegirá un valor de retorno arbitrario por usted.

Detalles de implementacion

Los comparadores se almacenan (como comparadores de objetos de 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 . En consecuencia, el estado de simulacro y de emparejamiento es estático pero tiene un alcance de subproceso de manera coherente entre las clases Mockito y Matchers.

La mayoría de las llamadas matcher sólo se suman a esta pila, con una excepción para los comparadores como and, or, ynot . Esto se corresponde perfectamente con (y se basa en) 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:

  1. Agregue anyInt()a la pila.
  2. Agregue gt(10)a la pila.
  3. Agregue lt(20)a la pila.
  4. Retire gt(10)y lt(20)y añadir and(gt(10), lt(20)).
  5. Llamar foo.quux(0, 0), que (a menos que se apruebe lo contrario) devuelve el valor predeterminado false. Internamente Mockito marca quux(int, int)como la llamada más reciente.
  6. Call when(false), que descarta su argumento y se prepara para el método stub quux(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 almacena el método con un any()comparador para su primer argumento y and(gt(10), lt(20))para su segundo argumento y borra la pila.

Esto demuestra algunas reglas:

  • Mockito no puede diferenciar entre quux(anyInt(), 0)y quux(0, anyInt()). Ambos parecen una llamada a quux(0, 0)con un int matcher en la pila. En consecuencia, si usa un comparador, debe hacer coincidir todos los argumentos.

  • El orden de las llamadas no solo es importante, es lo que hace que todo funcione . La extracción de comparadores a variables generalmente no funciona, porque generalmente cambia el orden de las llamadas. Sin embargo, la extracción de comparadores a 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 comparadores sin saber si se usan de inmediato o se abandonan accidentalmente. En teoría, la pila siempre debería estar vacía fuera de una llamada a wheno verify, pero Mockito no puede verificar eso automáticamente. Puede verificar manualmente con Mockito.validateMockitoUsage().

  • En una llamada a when, Mockito en realidad llama al método en cuestión, que generará una excepción si ha eliminado el método para generar una excepción (o requiere valores distintos de cero o no nulos). doReturny doAnswer(etc.) no invocan el método real y suelen ser una alternativa útil.

  • Si hubiera llamado a un método simulado en medio de un stubbing (por ejemplo, para calcular una respuesta para un eqcomparador), Mockito verificaría la longitud de la pila con esa llamada y probablemente fallaría.

  • Si intenta hacer algo malo, como apuntar / verificar un método final , Mockito llamará al método real y también dejará coincidencias adicionales en la pila . Es finalposible que la llamada al método no arroje una excepción, pero es posible que obtenga una InvalidUseOfMatchersException de los emparejadores perdidos la próxima vez que interactúe con un simulacro.

Problemas comunes

  • InvalidUseOfMatchersException :

    • Verifique que cada argumento tenga exactamente una llamada de emparejamiento, si es que usa emparejadores, y que no ha usado un emparejador fuera de una llamada wheno verify. Los comparadores nunca deben usarse como valores de retorno stubped o campos / variables.

    • Compruebe que no está llamando a un simulacro como parte de proporcionar un argumento de coincidencia.

    • Comprueba que no estás intentando apuntar / 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 burla es definitivo.

  • NullPointerException con argumentos primitivos: (Integer) any() devuelve nulo mientras any(Integer.class)devuelve 0; esto puede causar un NullPointerExceptionsi está esperando un en intlugar de un Integer. En cualquier caso, prefiera anyInt(), que devolverá cero y también saltará el paso de auto-boxing.

  • NullPointerException u otras excepciones: las llamadas a when(foo.bar(any())).thenReturn(baz)realmente llamarán foo.bar(null) , lo que podría haber detectado para generar una excepción al recibir un argumento nulo. Cambiar a doReturn(baz).when(foo).bar(any()) omite el comportamiento interrumpido .

Solución de problemas generales

  • Use MockitoJUnitRunner , o llame explícitamente validateMockitoUsagea su método tearDowno @After(lo que el corredor haría por usted automáticamente). Esto ayudará a determinar si ha hecho un mal uso de los emparejadores.

  • Para fines de depuración, agregue llamadas validateMockitoUsagedirectamente a su código. Esto arrojará si tiene algo en la pila, lo cual es una buena advertencia de un mal síntoma.

Jeff Bowman
fuente
2
Gracias por escribir esto. Una NullPointerException con el formato when / thenReturn me estaba causando problemas, hasta que lo cambié a doReturn / when.
yngwietiger
11

Solo una pequeña adición a la excelente respuesta de Jeff Bowman, ya que encontré esta pregunta cuando buscaba una solución a uno de mis propios problemas:

Si una llamada a un método coincide con más de las whenllamadas entrenadas de un simulacro , el orden de las whenllamadas 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 (probablemente) resultado deseado:

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

Si invierte las llamadas when, el resultado siempre será true.

tibtof
fuente
2
Aunque se trata de información útil, se refiere a stubbing, no a coincidencias , por lo que puede que no tenga sentido en esta pregunta. El orden sí importa, pero solo porque gana la última cadena de coincidencia definida : esto significa que los códigos auxiliares coexistentes a menudo se declaran de más específicos a menos, pero en algunos casos es posible que desee una anulación muy amplia del comportamiento específicamente simulado en un solo caso de prueba , momento en el que puede ser necesario que aparezca una definición amplia en último lugar.
Jeff Bowman
1
@JeffBowman Pensé que tiene sentido en esta pregunta, ya que la pregunta es acerca de los emparejadores de mockito y los emparejadores se pueden usar al hacer stubbing (como en la mayoría de sus ejemplos). Dado que buscar en Google una explicación me llevó a esta pregunta, creo que es útil tener esta información aquí.
tibtof