¿Cuándo usar Mockito.verify ()?


201

Escribo casos de prueba de jUnit para 3 propósitos:

  1. Para garantizar que mi código satisfaga toda la funcionalidad requerida, bajo todas (o la mayoría de) las combinaciones / valores de entrada.
  2. Para asegurarme de que puedo cambiar la implementación y confiar en los casos de prueba JUnit para decirme que toda mi funcionalidad aún está satisfecha.
  3. Como documentación de todos los casos de uso, mi código maneja y actúa como una especificación para la refactorización, en caso de que alguna vez sea necesario reescribir el código. (Refactorice el código, y si mis pruebas de jUnit fallan, probablemente haya perdido algún caso de uso).

No entiendo por qué o cuándo Mockito.verify()debería usarse. Cuando veo que me verify()llaman, me dice que mi jUnit se está dando cuenta de la implementación. (Por lo tanto, cambiar mi implementación rompería mis unidades, aunque mi funcionalidad no se haya visto afectada).

Estoy buscando:

  1. ¿Cuáles deberían ser las pautas para el uso apropiado de Mockito.verify()?

  2. ¿Es fundamentalmente correcto que jUnits sea consciente de la implementación de la clase bajo prueba o que esté estrechamente vinculada a ella?


1
Trato de evitar verificar () todo lo que puedo, por la misma razón que usted expuso (no quiero que mi prueba unitaria tome conciencia de la implementación), pero hay un caso en el que no tengo otra opción - métodos vacíos tropezados. En términos generales, como no devuelven nada, no contribuyen a su salida 'real'; pero aún así, debes saber que se llamó. Pero estoy de acuerdo con usted, no tiene sentido usar verificar para verificar el flujo de ejecución.
Legna

Respuestas:


78

Si el contrato de la clase A incluye el hecho de que llama al método B de un objeto de tipo C, entonces debe probarlo haciendo una simulación del tipo C y verificando que se haya llamado al método B.

Esto implica que el contrato de la clase A tiene suficientes detalles como para hablar sobre el tipo C (que podría ser una interfaz o una clase). Entonces, sí, estamos hablando de un nivel de especificación que va más allá de los "requisitos del sistema" y describe la implementación.

Esto es normal para las pruebas unitarias. Cuando realiza una prueba de unidad, desea asegurarse de que cada unidad esté haciendo "lo correcto", y eso generalmente incluirá sus interacciones con otras unidades. Las "unidades" aquí pueden significar clases o subconjuntos más grandes de su aplicación.

Actualizar:

Siento que esto no se aplica solo a la verificación, sino también al tropezar. Tan pronto como apriete un método de una clase de colaborador, su prueba de unidad se ha vuelto, en cierto sentido, dependiente de la implementación. Es algo así como la naturaleza de las pruebas unitarias. Dado que Mockito se trata tanto de tropezar como de verificar, el hecho de que esté usando Mockito implica que se encontrará con este tipo de dependencia.

En mi experiencia, si cambio la implementación de una clase, a menudo tengo que cambiar la implementación de sus pruebas unitarias para que coincida. Por lo general, sin embargo, no voy a tener que cambiar el inventario de lo que hay pruebas unitarias son de la clase; a menos, por supuesto, que la razón del cambio fuera la existencia de una condición que no pude probar antes.

Así que de esto se tratan las pruebas unitarias. Una prueba que no sufre este tipo de dependencia en la forma en que se usan las clases de colaboradores es realmente una prueba de subsistema o una prueba de integración. Por supuesto, estos también se escriben frecuentemente con JUnit, y con frecuencia implican el uso de burlas. En mi opinión, "JUnit" es un nombre terrible, para un producto que nos permite producir diferentes tipos de pruebas.


8
Gracias David Después de escanear algunos conjuntos de códigos, esto parece una práctica común, pero para mí, esto anula el propósito de crear pruebas unitarias y solo agrega la sobrecarga de mantenerlos por muy poco valor. Entiendo por qué se requieren simulacros y por qué las dependencias para ejecutar la prueba deben configurarse. Pero verificar que se ejecute el método dependencyA.XYZ () hace que las pruebas sean muy frágiles, en mi opinión.
Russell

@Russell ¿Incluso si "tipo C" es una interfaz para un contenedor alrededor de una biblioteca, o alrededor de algún subsistema distinto de su aplicación?
Dawood ibn Kareem

1
No diría que es completamente inútil garantizar que se invoque algún subsistema o servicio, solo que debería haber algunas pautas al respecto (formularlos era lo que quería hacer). Por ejemplo: (probablemente lo estoy simplificando demasiado). Digamos que estoy usando StrUtil.equals () en mi código y decido cambiar a StrUtil.equalsIgnoreCase () en la implementación. Si jUnit hubiera verificado (StrUtil.equals ), mi prueba podría fallar aunque la implementación es precisa. Esta llamada de verificación, IMO, es una mala práctica, aunque es para bibliotecas / subsistemas. Por otro lado, el uso de verificar para garantizar una llamada a closeDbConn podría ser un caso de uso válido.
Russell

1
Te entiendo y estoy completamente de acuerdo contigo. Pero también siento que escribir las pautas que usted describe podría expandirse a escribir un libro de texto TDD o BDD completo. Para tomar su ejemplo, llamar equals()o equalsIgnoreCase()nunca sería algo que se especificara en los requisitos de una clase, por lo que nunca tendría una prueba de unidad per se. Sin embargo, "cerrar la conexión de base de datos cuando se hace" (lo que sea que esto signifique en términos de implementación) puede ser un requisito de una clase, aunque no sea un "requisito comercial". Para mí, esto se reduce a la relación entre el contrato ...
Dawood ibn Kareem

... de una clase tal como se expresa en sus requisitos comerciales, y el conjunto de métodos de prueba que la unidad prueba esa clase. Definir esta relación sería un tema importante en cualquier libro sobre TDD o BDD. Mientras que alguien en el equipo de Mockito podría escribir una publicación sobre este tema para su wiki, no veo cómo diferiría de mucha otra literatura disponible. Si ve cómo puede diferir, avíseme y tal vez podamos trabajar juntos.
Dawood ibn Kareem

60

La respuesta de David es, por supuesto, correcta, pero no explica por qué querrías esto.

Básicamente, cuando la unidad de prueba está probando una unidad de funcionalidad de forma aislada. Usted prueba si la entrada produce la salida esperada. A veces, también debe probar los efectos secundarios. En pocas palabras, verificar le permite hacer eso.

Por ejemplo, tiene un poco de lógica empresarial que se supone que almacena cosas usando un DAO. Puede hacer esto usando una prueba de integración que crea instancias del DAO, lo conecta a la lógica de negocios y luego hurga en la base de datos para ver si las cosas esperadas se almacenaron. Esa ya no es una prueba unitaria.

O bien, podría burlarse del DAO y verificar que se llame de la manera que espera. Con mockito, puede verificar que se llama a algo, con qué frecuencia se lo llama e incluso usar coincidencias en los parámetros para asegurarse de que se llame de una manera particular.

La otra cara de las pruebas unitarias como esta es que está vinculando las pruebas a la implementación, lo que hace que la refactorización sea un poco más difícil. Por otro lado, un buen olor de diseño es la cantidad de código que se necesita para ejercerlo adecuadamente. Si sus pruebas necesitan ser muy largas, probablemente algo esté mal con el diseño. Por lo tanto, el código con muchos efectos secundarios / interacciones complejas que deben probarse probablemente no sea algo bueno.


30

Esta es una gran pregunta! Creo que la causa principal es la siguiente, estamos usando JUnit no solo para pruebas unitarias. Entonces la pregunta debería ser dividida:

  • ¿Debo usar Mockito.verify () en mi prueba de integración (o cualquier otra prueba superior a la unidad)?
  • ¿Debo usar Mockito.verify () en mi unidad de prueba de caja negra ?
  • ¿Debo usar Mockito.verify () en mi unidad de prueba de caja blanca ?

así que si ignoramos las pruebas superiores a la unidad, la pregunta puede reformularse: " Usar pruebas unitarias de caja blanca con Mockito.verify () crea una gran pareja entre la prueba unitaria y mi posible implementación, ¿puedo hacer algo de " caja gris? " pruebas unitarias y qué reglas generales debería usar para esto ".

Ahora, repasemos todo esto paso a paso.

* - ¿Debo usar Mockito.verify () en mi integración prueba de (o cualquier otra prueba superior a la unidad)? * Creo que la respuesta es claramente no, además no debes usar simulacros para esto. Su prueba debe estar lo más cerca posible de la aplicación real. Está probando un caso de uso completo, no una parte aislada de la aplicación.

* Recuadro negro vs caja blanca pruebas unitarias * Si está utilizando recuadro negro de enfoque de lo que es realmente haciendo, de alimentación (todas las clases de equivalencia) de entrada, un estado de salida, y las pruebas que va a recibir esperado. En este enfoque, el uso de simulacros en general está justificado (simplemente imitas que están haciendo lo correcto; no quieres probarlos), pero llamar a Mockito.verify () es superfluo.

Si está utilizando un enfoque de caja blanca, ¿qué está haciendo realmente? Está probando el comportamiento. de su unidad. En este enfoque, llamar a Mockito.verify () es esencial, debe verificar que su unidad se comporte como espera.

reglas generales para las pruebas de caja gris El problema con las pruebas de caja blanca es que crea un alto acoplamiento. Una posible solución es hacer pruebas de caja gris, no pruebas de caja blanca. Esta es una especie de combinación de pruebas de caja en blanco y negro. Realmente está probando el comportamiento de su unidad como en las pruebas de caja blanca, pero en general lo hace independiente de la implementación cuando es posible . Cuando sea posible, solo hará una verificación como en el caso de caja negra, solo afirma que la salida es lo que se espera que sea. Entonces, la esencia de su pregunta es cuándo es posible.

Esto es realmente dificil. No tengo un buen ejemplo, pero puedo darte ejemplos. En el caso que se mencionó anteriormente con equals () vs equalsIgnoreCase (), no debe llamar a Mockito.verify (), simplemente afirme la salida. Si no pudo hacerlo, divida su código en la unidad más pequeña, hasta que pueda hacerlo. Por otro lado, suponga que tiene algún @Service y está escribiendo @ Web-Service que es esencialmente un envoltorio en su @Service: delega todas las llamadas al @Service (y realiza un tratamiento adicional de errores). En este caso, llamar a Mockito.verify () es esencial, no debe duplicar todas las comprobaciones que hizo para @Serive, verificando que es suficiente llamar a @Service con la lista de parámetros correcta.


La prueba de caja gris es un poco una trampa. Tiendo a restringirlo a cosas como DAO. He estado en algunos proyectos con construcciones extremadamente lentas debido a la abundancia de pruebas de caja gris, una falta casi completa de pruebas de unidad y demasiadas pruebas de caja negra para compensar la falta de confianza en lo que supuestamente estaban probando las pruebas de caja gris.
Jilles van Gurp

Para mí, esta es la mejor respuesta disponible, ya que responde cuándo usar Mockito.when () en una variedad de situaciones. Bien hecho.
Michiel Leegwater

8

Debo decir que tiene toda la razón desde el punto de vista de un enfoque clásico:

  • Si primero crea (o cambia) la lógica de negocios de su aplicación y luego la cubre con (adopta) pruebas ( enfoque Prueba-Última ), entonces será muy doloroso y peligroso dejar que las pruebas sepan algo sobre cómo funciona su software, aparte de Comprobando entradas y salidas.
  • Si está practicando un enfoque basado en pruebas, sus pruebas son las primeras en escribirse, modificarse y reflejar los casos de uso de la funcionalidad de su software. La implementación depende de las pruebas. Eso a veces significa que desea que su software se implemente de una manera particular, por ejemplo, confíe en el método de algún otro componente o incluso lo llame una cantidad particular de veces. ¡Ahí es donde Mockito.verify () es útil!

Es importante recordar que no hay herramientas universales. El tipo de software, su tamaño, los objetivos de la empresa y la situación del mercado, las habilidades del equipo y muchas otras cosas influyen en la decisión sobre qué enfoque utilizar en su caso particular.


0

Como algunas personas dijeron

  1. A veces no tienes una salida directa en la que puedas afirmar
  2. A veces solo necesita confirmar que su método probado está enviando los resultados indirectos correctos a sus colaboradores (de lo que se está burlando).

Con respecto a su preocupación por romper sus pruebas al refactorizar, eso es algo esperado cuando se usan simulacros / trozos / espías. Lo digo por definición y no con respecto a una implementación específica como Mockito. Pero podría pensar de esta manera: si necesita hacer una refactorización que crearía cambios importantes en la forma en que funciona su método, es una buena idea hacerlo con un enfoque TDD, lo que significa que puede cambiar su prueba primero para definir el nuevo comportamiento (que fallará la prueba), y luego haga los cambios y vuelva a pasar la prueba.


0

En la mayoría de los casos, cuando a la gente no le gusta usar Mockito.verify, es porque se usa para verificar todo lo que está haciendo la unidad probada y eso significa que deberá adaptar su prueba si algo cambia en ella. Pero, no creo que sea un problema. Si desea poder cambiar lo que hace un método sin la necesidad de cambiar su prueba, eso básicamente significa que desea escribir pruebas que no prueben todo lo que está haciendo su método, porque no quiere que pruebe sus cambios . Y esa es la forma incorrecta de pensar.

Lo que realmente es un problema es si puede modificar lo que hace su método y una prueba unitaria que se supone que cubre la funcionalidad por completo no falla. Eso significaría que cualquiera que sea la intención de su cambio, el resultado de su cambio no está cubierto por la prueba.

Por eso, prefiero burlarme tanto como sea posible: también burlarme de sus objetos de datos. Al hacerlo, no solo puede usar la verificación para verificar que se invocan los métodos correctos de otras clases, sino también que los datos que se pasan se recopilan a través de los métodos correctos de esos objetos de datos. Y para completarlo, debe probar el orden en que ocurren las llamadas. Ejemplo: si modifica un objeto de entidad db y luego lo guarda usando un repositorio, no es suficiente verificar que los establecedores del objeto sean llamados con los datos correctos y que se llame al método guardar del repositorio. Si se llaman en el orden incorrecto, su método aún no hace lo que debería hacer. Por lo tanto, no uso Mockito.verify, pero creo un objeto inOrder con todos los simulacros y uso inOrder.verify en su lugar. Y si desea completarlo, también debe llamar a Mockito. verifique NoMoreInteractions al final y páselo todos los simulacros. De lo contrario, alguien puede agregar una nueva funcionalidad / comportamiento sin probarlo, lo que significaría después de que sus estadísticas de cobertura pueden ser del 100% y aún así está acumulando código que no se afirma o verifica.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.