¿Cómo funcionan los comparadores de Mockito?

Resuelto Jeff Bowman asked hace 10 años • 2 respuestas

Los comparadores de argumentos Mockito (como any, argThat, eq, samey 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 Answers 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?

Jeff Bowman avatar Apr 03 '14 03:04 Jeff Bowman
Aceptado

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 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 Tdonde 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, gty startsWithon org.mockito.Matchersand org.mockito.AdditionalMatchers. También hay adaptadores que han cambiado en las versiones de Mockito:

  • Para Mockito 1.x, Matchersalgunas llamadas destacadas (como intThato argThat) son comparadores de Mockito que aceptan directamente 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 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. Matchersllamadas redactadas intThato argThatenvueltas como 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 mudado a MockitoHamcrestsu 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.setPowerLeveles un método que acepta un int. is(greaterThan(9000))devuelve a Matcher<Integer>, que no funcionaría como setPowerLevelargumento. El comparador Mockito intThatenvuelve ese Matcher estilo Hamcrest y devuelve un intpara 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 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 llamas a un comparador como anyo 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 nullno 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 eqygt 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, orynot . 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:

  1. Añadir anyInt()a la pila.
  2. Añadir gt(10)a la pila.
  3. Añadir lt(20)a la pila.
  4. Retirar gt(10)y lt(20)agregar and(gt(10), lt(20)).
  5. Call foo.quux(0, 0), que (a menos que se indique lo contrario) devuelve el valor predeterminado false. Marcas internas de Mockitoquux(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 bloquea el método con un any()comparador para su primer argumento y and(gt(10), lt(20))para su segundo argumento y limpia la pila.

Esto demuestra algunas reglas:

  • Mockito no puede distinguir 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 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 wheno verify, pero Mockito no puede comprobarlo automáticamente. Puedes comprobarlo manualmente con Mockito.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). 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 una prueba (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 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 finalllamada 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 wheno . verifyLas 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 mientras any(Integer.class)devuelve 0; esto puede causar un NullPointerExceptionsi espera un inten lugar de un número entero. En cualquier caso, prefiera anyInt(), 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án foo.bar(null) a , lo que podrías haber bloqueado para generar una excepción al recibir un argumento nulo. Cambiar a doReturn(baz).when(foo).bar(any()) omite el comportamiento de trozos .

Solución de problemas generales

  • Utilice 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 has hecho un mal uso de los comparadores.

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

Jeff Bowman avatar Apr 02 '2014 20:04 Jeff Bowman

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 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 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.

tibtof avatar Nov 05 '2015 14:11 tibtof