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í .
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í .
Respuestas:
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.
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 MailServer
interfaz. Aquí es cuando podemos usar objetos simulados.
Con los objetos simulados, en lugar de pasar un real MailServerImpl
o una prueba TestMailServer
, podemos pasar una implementación simulada de la MailServer
interfaz. 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.
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 MySorter
clase 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.
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 PdfFormatter
objeto 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 PdfFormatter
objeto, 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 PdfFormatter
se 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:
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.TestPdfFormatter
que 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 .
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.
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í