¿Cuándo debo burlarme?

138

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

Esteban Araya
fuente
Recomiendo burlarse de las dependencias fuera del proceso y solo aquellas que tengan interacciones externas (servidor SMTP, bus de mensajes, etc.). No te burles de la base de datos, es un detalle de implementación. Más sobre esto aquí: enterprisecraftsmanship.com/posts/when-to-mock
Vladimir

Respuestas:

122

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

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

Si su dependencia es defectuosa, su prueba puede verse afectada de tal manera que devuelva un falso positivo. Por ejemplo, puede pasar la dependencia a un nulo inesperado, y la dependencia no puede arrojar un valor nulo como está documentado. Su prueba no encuentra una excepción de argumento nulo como debería, y la prueba pasa.

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

Un simulacro reemplaza esa dependencia. Establece expectativas en las llamadas al objeto dependiente, establece los valores de retorno exactos que debe darle para realizar la prueba que desea y / o qué excepciones lanzar 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: se burlan de cada dependencia que toca su prueba de unidad.

Drew Stephens
fuente
164
Esta respuesta es demasiado radical. Las pruebas unitarias pueden y deben ejercer más de un método único, siempre que todo pertenezca a la misma unidad cohesiva. Hacer lo contrario requeriría demasiadas burlas / falsificaciones, lo que llevaría a pruebas complicadas y frágiles. Solo las dependencias que realmente no pertenecen a la unidad bajo prueba deben reemplazarse mediante burlas.
Rogério
10
Esta respuesta también es demasiado optimista. Sería mejor si incorporase las deficiencias de @ Jan de objetos simulados.
Jeff Axelrod
1
¿No es esto más un argumento para inyectar dependencias para pruebas en lugar de simulacros específicamente? Podrías reemplazar "simulacro" con "trozo" en tu respuesta. Estoy de acuerdo en que debe burlarse o tropezar con las dependencias significativas. He visto un montón de códigos simulados que básicamente terminan reimplementando partes de los objetos burlados; las burlas ciertamente no son una bala de plata.
Draemon
2
Burla de cada dependencia que toca tu prueba de unidad. Esto explica todo.
Teoman shipahi
2
TL; DR: se burlan de cada dependencia que toca su prueba de unidad. - este no es realmente un gran enfoque, dice el propio mockito: no te burles de todo. (downvoted)
p_champ
167

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

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

Con los objetos simulados, en lugar de pasar un real MailServerImplo una prueba TestMailServer, podemos pasar una implementación simulada de la MailServerinterfaz. Antes de pasar un simulacro MailServer, lo "entrenamos" para que sepa qué método requiere esperar y qué valores de retorno 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.

Deficiencias simuladas

Si tiene un marco simulado en su lugar, está tentado a usar un objeto simulado cada vez que necesite pasar una interfaz a la clase bajo la prueba. De esta manera, terminas probando interacciones incluso cuando no es necesario . Desafortunadamente, las pruebas no deseadas (accidentales) de las interacciones son malas, porque estás probando que un requisito 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 MySorterclase 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 la clasificación rápida, lo que queremos probar; en ese caso, la última prueba sería realmente válida).

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

Simulacros de trozos

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

Supongamos que tenemos un método sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)que queremos probar. El PdfFormatterobjeto 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 PdfFormatterobjeto, por lo que lo entrenamos para aceptar silenciosamente cualquier llamada y devolver algunos valores de retorno enlatados razonables para todos los métodos que sendInvitation()suceden 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 entrenamos el código auxiliar para responder a un método sin tener una idea de por qué necesita llamarlo, simplemente agregamos todo de lo que se quejó la prueba. Estamos contentos, la prueba pasa.

Pero, ¿qué sucede más tarde, cuando cambiamos sendInvitations(), o alguna otra clase que sendInvitations()usa, para crear archivos PDF más elegantes? Nuestra prueba falla repentinamente porque ahora PdfFormatterse llaman más métodos y no entrenamos a nuestro trozo para esperarlos. Y generalmente no es solo una prueba que falla en situaciones como esta, es cualquier prueba que utiliza, directa o indirectamente, el sendInvitations()método. 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 son necesarios, porque no sabemos cuáles de ellos no son necesarios. Nuevamente, dificulta la refactorización.

Además, la legibilidad de la prueba sufrió terriblemente, hay mucho código allí que no escribimos porque queríamos, sino porque teníamos que hacerlo; No somos nosotros quienes queremos ese código allí. Las pruebas que usan objetos simulados 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 mantendrá; de hecho, es más fácil eliminarlos que mantenerlos.

¿Cómo arreglar eso? Fácilmente:

  • Intenta usar clases reales en lugar de simulacros siempre que sea posible. Usa lo real PdfFormatterImpl. Si no es posible, cambie las clases reales para que sea posible. No poder usar una clase en las pruebas generalmente apunta a algunos problemas con la clase. Solucionar los problemas es una situación en la que todos ganan: arreglaste la clase y tienes una prueba más simple. Por otro lado, no arreglarlo y usar simulacros es una situación de no ganar: no solucionó la clase real y tiene pruebas más complejas y menos legibles que dificultan nuevas 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. Crea TestPdfFormatterque no hace nada. De esa manera, puede cambiarlo una vez para todas las pruebas y sus pruebas no están llenas de configuraciones largas donde entrena sus talones.

En general, los objetos simulados tienen su uso, pero cuando no se usan con cuidado, a menudo fomentan 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 .

Jan Soltis
fuente
1
Una respuesta bien pensada, y en general estoy de acuerdo. Diría que, dado que las pruebas unitarias son pruebas de caja blanca, tener que cambiar las pruebas cuando cambia la implementación para enviar archivos PDF más sofisticados puede no ser una carga irrazonable. A veces, los simulacros pueden ser una forma útil de implementar rápidamente trozos en lugar de tener muchas placas de caldera. Sin embargo, en la práctica parece que su uso no está restringido a estos casos simples.
Draemon
1
¿No es el objetivo de un simulacro que sus pruebas sean consistentes, que no tenga que preocuparse por burlarse de objetos cuyas implementaciones cambian continuamente posiblemente por otros programadores cada vez que ejecuta su prueba y obtiene resultados consistentes?
PositiveGuy
1
Puntos muy buenos y relevantes (especialmente sobre pruebas de fragilidad). Solía usar burla mucho cuando era más joven, pero ahora considero prueba de unidad que heavilly dependen de burla como potencialmente desechable y se centran más en las pruebas de integración (con componentes reales)
Kemoda
66
"No poder usar una clase en las pruebas generalmente apunta a algunos problemas con la clase". Si la clase es un servicio (por ejemplo, acceso a la base de datos o proxy al servicio web), debe considerarse como una dependencia externa y burlarse /
tropezarse
1
Pero, ¿qué sucede más tarde, cuando cambiamos sendInvitations ()? Si el código bajo prueba se modifica, ya no garantiza el contrato anterior, por lo tanto, tiene que fallar. Y generalmente no es solo una prueba que falla en situaciones como esta . Si este es el caso, el código no está limpio implementado. La verificación de las llamadas al método de la dependencia debe probarse solo una vez (en la prueba de unidad apropiada). Todas las demás clases usarán solo la instancia simulada. Por lo tanto, no veo ningún beneficio al mezclar la integración con las pruebas unitarias.
Christopher Will
55

Regla de oro:

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

Orion Edwards
fuente
4

Debe burlarse de un objeto cuando tiene una dependencia en una unidad de código que está tratando de probar que debe ser "exactamente así".

Por ejemplo, cuando intenta probar algo de 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: simule ese objeto.

Puede encontrar un gran podcast sobre el tema aquí

Toran Billups
fuente
El enlace ahora se dirige al episodio actual, no al episodio deseado. ¿El podcast previsto es hanselminutes.com/32/mock-objects ?
C Perkins