¿Cómo funciona la invocación de mockito when ()?

111

Dada la siguiente declaración de Mockito:

when(mock.method()).thenReturn(someValue);

¿Cómo hace Mockito para crear un proxy para un simulacro, dado que la declaración mock.method () pasará el valor de retorno a when ()? Me imagino que esto usa algunas cosas de CGLib, pero estaría interesado en saber cómo se hace técnicamente.

marchaos
fuente

Respuestas:

118

La respuesta corta es que en su ejemplo, el resultado de mock.method()será un valor vacío apropiado para el tipo; mockito usa indirección a través de proxy, interceptación de métodos y una instancia compartida de la MockingProgressclase para determinar si una invocación de un método en un simulacro es para apuntar o reproducir un comportamiento stubbed existente en lugar de pasar información sobre stubbing a través del valor de retorno de un método burlado.

Un mini-análisis en un par de minutos mirando el código mockito es el siguiente. Tenga en cuenta que esta es una descripción muy aproximada: hay muchos detalles en juego aquí. Le sugiero que consulte la fuente en github usted mismo.

Primero, cuando te burlas de una clase usando el mockmétodo de la Mockitoclase, esto es esencialmente lo que sucede:

  1. Mockito.mockdelegados a org.mockito.internal.MockitoCore.mock, pasando la configuración de simulación predeterminada como parámetro.
  2. MockitoCore.mockdelegados a org.mockito.internal.util.MockUtil.createMock
  3. La MockUtilclase usa la ClassPathLoaderclase para obtener una instancia de MockMakerusar para crear el simulacro. De forma predeterminada, se utiliza la clase CgLibMockMaker .
  4. CgLibMockMakerusa una clase prestada de JMock, ClassImposterizerque se encarga de crear el simulacro. Las piezas clave de la 'magia mockito' utilizadas son las MethodInterceptorutilizadas para crear el mock: el mockito MethodInterceptorFiltery una cadena de instancias de MockHandler, incluida una instancia de MockHandlerImpl . El interceptor de métodos pasa las invocaciones a la instancia de MockHandlerImpl, que implementa la lógica de negocios que se debe aplicar cuando se invoca un método en un simulacro (es decir, buscar para ver si una respuesta ya está registrada, determinar si la invocación representa un nuevo stub, etc. El estado predeterminado es que si un código auxiliar aún no está registrado para el método que se invoca, se devuelve un valor vacío apropiado para el tipo .

Ahora, veamos el código en su ejemplo:

when(mock.method()).thenReturn(someValue)

Este es el orden en el que se ejecutará este código:

  1. mock.method()
  2. when(<result of step 1>)
  3. <result of step 2>.thenReturn

La clave para comprender lo que está sucediendo es lo que sucede cuando se invoca el método en el simulacro: al interceptor del método se le pasa información sobre la invocación del método, y delega en su cadena de MockHandlerinstancias, que eventualmente delega en MockHandlerImpl#handle. Durante MockHandlerImpl#handle, el controlador simulado crea una instancia de OngoingStubbingImply la pasa a la MockingProgressinstancia compartida .

Cuando el whenmétodo se invoca después de la invocación de method(), delega en MockitoCore.when, que llama al stub()método de la misma clase. Este método descomprime el stubbing en curso de la MockingProgressinstancia compartida en la que method()escribió la invocación simulada y lo devuelve. Luego thenReturn, se llama al método en la OngoingStubbinginstancia.

Paul Morie
fuente
1
Gracias por la respuesta detallada. Otra pregunta: mencionas que "cuando se invoca el método después de la invocación del método ()", ¿cómo sabe que la invocación de when () es la siguiente invocación (o envuelve) la invocación del método ()? Espero que tenga sentido.
marchaos
@marchaos No lo sabe. Con la when(mock.method()).thenXyz(...)sintaxis, mock.method()se ejecuta en modo "replay", no en modo "stubbing". Normalmente, esta ejecución de mock.method()no tiene ningún efecto, por lo que más tarde, cuando thenXyz(...)( thenReturn, thenThrow, thenAnswer, etc.) será ejecutado, entra en "apagando" el modo y luego graba el resultado deseado para esa llamada al método.
Rogério
1
Rogerio, en realidad es un poco más sutil que eso: mockito no tiene modos explícitos de reproducción y reproducción. Editaré mi respuesta más tarde para que quede más claro.
Paul Morie
Resumiría que, en resumen, es más fácil interceptar una llamada a un método dentro de otro método con CGLIB o Javassist que interceptar, digamos, el operador "if".
Infeligo
Todavía no he modificado mi descripción aquí, pero tampoco me he olvidado de ella. FYI.
Paul Morie
33

La respuesta corta es, detrás de escena, Mockito usa algún tipo de variables / almacenamiento globales para guardar información de los pasos de construcción del código auxiliar del método (invocación del método (), when (), thenReturn () en su ejemplo), de modo que eventualmente pueda construya un mapa sobre lo que debe devolverse cuando lo que se llama en qué param.

Encontré este artículo muy útil: Explicación de cómo funcionan los marcos simulados basados ​​en proxy ( http://blog.rseiler.at/2014/06/explanation-how-proxy-based-mock.html ). El autor implementó un marco de demostración de Mocking, que encontré un muy buen recurso para las personas que quieren descubrir cómo funcionan estos marcos de Mocking.

En mi opinión, es un uso típico de Anti-Pattern. Normalmente deberíamos evitar los 'efectos secundarios' cuando implementamos un método, lo que significa que el método debería aceptar la entrada y hacer algunos cálculos y devolver el resultado; nada más cambió además de eso. Pero Mockito simplemente viola esa regla a propósito. Sus métodos almacenan un montón de información además de devolver el resultado: Mockito.anyString (), mockInstance.method (), when (), thenReturn, todos tienen un 'efecto secundario' especial. Es también por eso que el marco parece una magia a primera vista; por lo general, no escribimos código como ese. Sin embargo, en el caso del marco burlón, este diseño anti-patrón es un gran diseño, ya que conduce a una API muy simple.

Jacob Wu
fuente
4
Excelente enlace. La genialidad detrás de esto es: La API muy simple que hace que todo se vea muy bien. Otra gran decisión es que el método when () usa genéricos para que el método thenReturn () sea seguro para los tipos.
David Tonhofer
Considero que esta es la mejor respuesta. En contraste con la otra respuesta, explica claramente los conceptos del proceso de burla en lugar del flujo de control a través de un código concreto. Estoy de acuerdo, excelente enlace.
mihca