unit theme test online dummy development data codex code unit-testing language-agnostic mocking

unit testing - theme - ¿Cuándo debería burlarme?



wordpress simulator (4)

Tengo una comprensión básica de los objetos simulados y falsos, pero no estoy seguro de tener una idea de cuándo / dónde usar el burlarse, especialmente porque se aplicaría a este escenario here .


Debería burlarse de un objeto cuando tiene una dependencia en una unidad de código que está tratando de probar que necesita ser "así".

Por ejemplo, cuando intenta probar alguna lógica en su unidad de código pero necesita obtener algo de otro objeto y lo que devuelve esta dependencia puede afectar lo que está tratando de probar: simular ese objeto.

Un gran podcast sobre el tema se puede encontrar here


Los objetos simulados son útiles cuando desea probar las interacciones entre una clase bajo prueba y una interfaz particular.

Por ejemplo, queremos probar ese método sendInvitations(MailServer mailServer) llama a MailServer.createMessage() exactamente una vez, y también llama a MailServer.sendMessage(m) exactamente una vez, y no se invoca ningún otro método en la interfaz de MailServer . Aquí es cuando podemos usar objetos simulados.

Con objetos simulados, en lugar de pasar un MailServerImpl real, o una prueba TestMailServer , podemos aprobar una implementación simulada de la interfaz MailServer . Antes de pasar un MailServer falso, lo "entrenamos", para que sepa qué llamadas de método esperar y qué valores devueltos devolver. Al final, el objeto simulado afirma que todos los métodos esperados se llamaron como se esperaba.

Esto suena bien en teoría, pero también hay algunas desventajas.

Fallas falsas

Si tiene un marco simulado en su lugar, tiene la tentación de usar objetos simulados cada vez que necesita pasar una interfaz a la clase bajo la prueba. De esta forma terminas probando interacciones incluso cuando no es necesario . Desafortunadamente, las pruebas no deseadas (accidentales) de las interacciones son malas, porque luego se prueba que un requisito en particular se implementa de una manera particular, en lugar de que la implementación produzca el resultado requerido.

Aquí hay un ejemplo en pseudocódigo. Supongamos que hemos creado una clase MySorter y queremos probarla:

// the correct way of testing testSort() { testList = [1, 7, 3, 8, 2] MySorter.sort(testList) assert testList equals [1, 2, 3, 7, 8] } // incorrect, testing implementation testSort() { testList = [1, 7, 3, 8, 2] MySorter.sort(testList) assert that compare(1, 2) was called once assert that compare(1, 3) was not called assert that compare(2, 3) was called once .... }

(En este ejemplo, suponemos que no es un algoritmo de clasificación particular, como el ordenamiento rápido, lo que queremos probar, en ese caso, la última prueba sería en realidad válida).

En un ejemplo tan extremo, es obvio por qué el último ejemplo es incorrecto. Cuando cambiamos la implementación de MySorter , la primera prueba hace un gran trabajo para asegurarnos de que todavía ordenamos correctamente, que es el objetivo de las pruebas: nos permiten cambiar el código de forma segura. Por otro lado, la última prueba siempre se rompe y es activamente dañina; obstaculiza la refactorización.

Se burla de los talones

Los marcos falsos a menudo también permiten un uso menos estricto, donde no tenemos que especificar exactamente cuántas veces se deben llamar los métodos y qué parámetros se esperan; Permiten la creación de objetos simulados que se utilizan como stubs .

Supongamos que tenemos un método sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer) que queremos probar. El objeto PdfFormatter se puede usar para crear la invitación. Aquí está la prueba:

testInvitations() { // train as stub pdfFormatter = create mock of PdfFormatter let pdfFormatter.getCanvasWidth() returns 100 let pdfFormatter.getCanvasHeight() returns 300 let pdfFormatter.addText(x, y, text) returns true let pdfFormatter.drawLine(line) does nothing // train as mock mailServer = create mock of MailServer expect mailServer.sendMail() called exactly once // do the test sendInvitations(pdfFormatter, mailServer) assert that all pdfFormatter expectations are met assert that all mailServer expectations are met }

En este ejemplo, realmente no nos importa el objeto PdfFormatter por lo que simplemente lo capacitamos para aceptar silenciosamente cualquier llamada y devolver algunos valores razonables de retorno sendInvitation() para todos los métodos a los que sendInvitation() llama en este momento. ¿Cómo se nos ocurrió exactamente esta lista de métodos para entrenar? Simplemente ejecutamos la prueba y seguimos agregando los métodos hasta que pasó la prueba. Tenga en cuenta que hemos entrenado el código auxiliar para responder a un método sin tener una idea de por qué necesita llamarlo, simplemente agregamos todo lo que la prueba se quejó. Estamos felices, la prueba pasa.

Pero, ¿qué pasará más tarde, cuando cambiemos sendInvitations() , o alguna otra clase que sendInvitations() , para crear más sofisticados pdfs? Nuestra prueba falla de repente porque ahora se llaman más métodos de PdfFormatter y no entrenamos nuestro stub para esperarlos. Y, por lo general, no es solo una prueba que falla en situaciones como esta, cualquier prueba que use, directa o indirectamente, el método sendInvitations() . Tenemos que arreglar todas esas pruebas agregando más entrenamientos. También tenga en cuenta que no podemos eliminar los métodos que ya no necesitamos, ya que no sabemos cuáles de ellos no son necesarios. De nuevo, dificulta la refactorización.

Además, la legibilidad de la prueba sufrió terriblemente, hay muchos códigos allí que no escribimos porque queríamos, pero porque teníamos que hacerlo; no somos nosotros los que queremos ese código allí. Las pruebas que usan objetos falsos se ven muy complejas y, a menudo, son difíciles de leer. Las pruebas deben ayudar al lector a comprender cómo debe usarse la clase bajo la prueba, por lo tanto, deben ser simples y directas. Si no son legibles, nadie los va a mantener; de hecho, es más fácil eliminarlos que mantenerlos.

¿Cómo arreglar eso? Fácilmente:

  • Intenta utilizar clases reales en lugar de burlas siempre que sea posible. Use el PdfFormatterImpl real. Si no es posible, cambie las clases reales para que sea posible. No poder usar una clase en las pruebas usualmente apunta a algunos problemas con la clase. La solución de los problemas es una situación en la que todos salen ganando: arregló la clase y tiene una prueba más sencilla. Por otro lado, no arreglarlo y usar burlas es una situación sin salida: no corrigió la clase real y tiene pruebas más complejas y menos legibles que impiden futuras refactorizaciones.
  • Intente crear una implementación de prueba simple de la interfaz en lugar de burlarse de ella en cada prueba, y use esta clase de prueba en todas sus pruebas. Crear TestPdfFormatter que no hace nada. De esta manera, puede cambiarlo una vez para todas las pruebas y sus pruebas no se llenan con configuraciones largas en las que entrena sus talones.

En general, los objetos simulados tienen su uso, pero cuando no se usan con cuidado, a menudo fomentan las malas prácticas, prueban los detalles de implementación, dificultan la refactorización y producen pruebas difíciles de leer y difíciles de mantener .

Para obtener más detalles sobre las deficiencias de los simulacros, consulte también Objetos simulados: deficiencias y casos de uso .


Regla de oro:

Si la función que está probando necesita un objeto complicado como parámetro, y sería una pena simplemente crear una instancia de este objeto (si, por ejemplo, intenta establecer una conexión TCP), utilice un simulacro.


Una prueba de unidad debe probar una única ruta de codificación a través de un único método. Cuando la ejecución de un método pasa fuera de ese método, a otro objeto y viceversa, tiene una dependencia.

Cuando prueba esa ruta de código con la dependencia real, no está realizando pruebas unitarias; eres una prueba de integración Si bien eso es bueno y necesario, no se trata de pruebas unitarias.

Si su dependencia tiene errores, su prueba puede verse afectada de tal manera que devuelva un resultado falso positivo. Por ejemplo, puede pasarle a la dependencia un nulo inesperado, y la dependencia puede no arrojar un nulo, como está documentado. Su prueba no incluye una excepción de argumento nulo como debería, y la prueba pasa.

Además, puede ser difícil, si no imposible, obtener de manera confiable que el objeto dependiente devuelva exactamente lo que desea durante una prueba. Eso también incluye arrojar excepciones esperadas dentro de las pruebas.

Un simulacro reemplaza esa dependencia. Establece expectativas en las llamadas al objeto dependiente, establece los valores de devolución exactos que le deberían dar para realizar la prueba que desea y / o qué excepciones arrojar para que pueda probar su código de manejo de excepciones. De esta forma, puede probar la unidad en cuestión fácilmente.

TL; DR: simula todas las dependencias que toca su prueba de unidad.