¿Cómo detecta problemas de dependencia con las pruebas unitarias cuando usa objetos simulados?

95

Tienes una clase X y escribes algunas pruebas unitarias que verifican el comportamiento X1. También hay una clase A que toma X como una dependencia.

Cuando escribes pruebas unitarias para A, te burlas de X. En otras palabras, mientras pruebas la unidad A, configuras (postulas) el comportamiento de la simulación de X como X1. El tiempo pasa, la gente usa su sistema, necesita cambiar, X evoluciona: modifica X para mostrar el comportamiento X2. Obviamente, las pruebas unitarias para X fallarán y tendrá que adaptarlas.

Pero ¿qué pasa con A? Las pruebas unitarias para A no fallarán cuando se modifique el comportamiento de X (debido a la burla de X). ¿Cómo detectar que el resultado de A será diferente cuando se ejecute con la X "real" (modificada)?

Estoy esperando respuestas a lo largo de la línea de: "Ese no es el propósito de la prueba de unidad", pero ¿qué valor tiene la prueba de unidad entonces? ¿Realmente solo le dice que cuando todas las pruebas pasan, no ha introducido un cambio de ruptura? Y cuando el comportamiento de alguna clase cambia (voluntaria o involuntariamente), ¿cómo puede detectar (preferiblemente de manera automatizada) todas las consecuencias? ¿No deberíamos centrarnos más en las pruebas de integración?

    
pregunta bvgheluwe 27.03.2018 - 13:53
fuente

11 respuestas

125
  

Cuando escribes pruebas unitarias para A, te burlas de X

¿Tú? No lo hago, a menos que sea absolutamente necesario. Tengo que si:

  1. X es lento, o
  2. X tiene efectos secundarios

Si ninguno de estos se aplica, entonces mis pruebas de unidad de A también probarán X . Hacer cualquier otra cosa sería llevar las pruebas de aislamiento a un extremo ilógico.

Si tiene partes de su código utilizando simulacros de otras partes de su código, entonces estoy de acuerdo: ¿cuál es el punto de tales pruebas de unidad? Así que no hagas esto. Deje que esas pruebas usen las dependencias reales, ya que forman pruebas mucho más valiosas de esa manera.

Y si algunas personas se enojan contigo al llamar a estas pruebas, "pruebas unitarias", solo tienes que llamarlas "pruebas automatizadas" y seguir escribiendo buenas pruebas automatizadas.

    
respondido por el David Arno 27.03.2018 - 14:29
fuente
77

Necesitas ambos. Pruebas unitarias para verificar el comportamiento de cada una de sus unidades, y algunas pruebas de integración para asegurarse de que estén conectadas correctamente. El problema de confiar solo en las pruebas de integración es la explosión combinatoria que resulta de las interacciones entre todas sus unidades.

Supongamos que tiene clase A, que requiere 10 pruebas de unidad para cubrir todas las rutas. Luego tienes otra clase B, que también requiere 10 pruebas de unidad para cubrir todas las rutas que el código puede tomar a través de ella. Ahora digamos que en su aplicación, necesita alimentar la salida de A a B. Ahora su código puede tomar 100 rutas diferentes desde la entrada de A hasta la salida de B.

Con las pruebas unitarias, solo necesitas 20 pruebas unitarias + 1 prueba de integración para cubrir completamente todos los casos.

Con las pruebas de integración, necesitarás 100 pruebas para cubrir todas las rutas de código.

Aquí hay un muy buen video sobre las desventajas de confiar en las pruebas de integración Las pruebas integradas de JB Rainsberger son una estafa HD >

    
respondido por el Eternal21 27.03.2018 - 14:22
fuente
72
  

Cuando escribes pruebas unitarias para A, te burlas de X. En otras palabras, mientras pruebas la unidad A, configuras (postulas) el comportamiento de la simulación de X como X1. El tiempo pasa, la gente usa su sistema, necesita cambiar, X evoluciona: modifica X para mostrar el comportamiento X2. Obviamente, las pruebas unitarias para X fallarán y tendrá que adaptarlas.

Woah, espera un momento. Las implicaciones de las pruebas para el fracaso de X son demasiado importantes para pasar por alto así.

Si cambiar la implementación de X de X1 a X2 rompe las pruebas unitarias para X, eso indica que ha realizado un cambio incompatible con el contrato X hacia atrás.

X2 no es una X, en Liskov sense , por lo que debería pensar sobre otras formas de satisfacer las necesidades de sus partes interesadas (como introducir una nueva especificación Y, implementada por X2).

Para obtener información más detallada, vea Pieter Hinjens: El fin de las versiones de software , o Rich Hickey Simple Made Easy .

Desde la perspectiva de A, hay una condición previa que el colaborador respeta el contrato X. Y su observación es efectiva que la prueba aislada para A no le da ninguna seguridad de que A reconozca a los colaboradores que violan el contrato X.

Revise Las pruebas integradas son una estafa ; en un nivel alto, se espera que tenga tantas pruebas aisladas como sea necesario para asegurarse de que X2 implementa el contrato X correctamente, y tantas pruebas aisladas como sea necesario para asegurarse de que A haga lo correcto dadas las respuestas interesantes de una X, y un número menor de pruebas integradas para garantizar que X2 y A estén de acuerdo con lo que significa X.

A veces verá esta distinción expresada como pruebas solitary frente a sociable tests ; vea Jay Fields Cómo trabajar eficazmente con pruebas unitarias .

  

¿No deberíamos centrarnos más en las pruebas de integración?

Nuevamente, vea que las pruebas integradas son una estafa. Rainsberger describe en detalle un circuito de retroalimentación positiva que es común (en su experiencia) a proyectos que se basan en pruebas integradas (ortografía de notas). En resumen, sin las pruebas aisladas / solitarias aplicando presión al diseño , la calidad se degrada, lo que lleva a más errores y pruebas más integradas ...

También necesitarás (algunas) pruebas de integración. Además de la complejidad introducida por los múltiples módulos, la ejecución de estas pruebas tiende a tener más resistencia que las pruebas aisladas; es más eficiente realizar iteraciones en las comprobaciones muy rápidas cuando el trabajo está en progreso, guardando las comprobaciones adicionales para cuando crees que estás "listo".

    
respondido por el VoiceOfUnreason 27.03.2018 - 16:01
fuente
15

Permítanme comenzar diciendo que la premisa central de la pregunta es errónea.

Nunca está probando (o burlándose) de implementaciones, está probando (y burlándose) de interfaces .

Si tengo una clase X real que implementa la interfaz X1, puedo escribir una XM simulada que también cumple con X1. Entonces mi clase A debe usar algo que implemente X1, que puede ser clase X o simulacro de XM.

Ahora, supongamos que cambiamos X para implementar una nueva interfaz X2. Bueno, obviamente mi código ya no compila. A requiere algo que implemente X1, y que ya no existe. El problema se ha identificado y se puede solucionar.

Supongamos que en lugar de reemplazar X1, simplemente lo modificamos. Ahora la clase A está lista. Sin embargo, el XM simulado ya no implementa la interfaz X1. El problema se ha identificado y se puede solucionar.

La base completa para la prueba de unidad y la burla es que escribe código que usa interfaces. A un consumidor de una interfaz no le importa cómo se implementa el código, solo que se cumpla el mismo contrato (entradas / salidas).

Esto se descompone cuando tus métodos tienen efectos secundarios, pero creo que se puede excluir de forma segura ya que "no se pueden probar ni simular" ".     

respondido por el Vlad274 27.03.2018 - 19:40
fuente
9

A su vez, tomando sus preguntas:

  

qué valor tienen las pruebas unitarias

Son baratos para escribir y ejecutar, y obtienes comentarios tempranos. Si rompe X, descubrirá más o menos inmediatamente si tiene buenas pruebas. Ni siquiera considere la posibilidad de escribir pruebas de integración a menos que haya probado todas las capas (sí, incluso en la base de datos).

  

¿Realmente solo te dice que cuando todas las pruebas pasan, no lo has hecho?   introdujo un cambio de ruptura

Tener exámenes que pasan realmente podría decirte muy poco. Es posible que no hayas escrito suficientes pruebas. Es posible que no haya probado suficientes escenarios. La cobertura del código puede ayudar aquí, pero no es una bala de plata. Es posible que tenga pruebas que siempre aprueben. Por lo tanto, el rojo es el primer paso, a menudo pasado por alto, de refactor rojo, verde.

  

Y cuando el comportamiento de alguna clase cambia (voluntaria o involuntariamente),   ¿Cómo se puede detectar (preferiblemente de forma automatizada) todas las   consecuencias

Más pruebas, aunque las herramientas mejoran cada vez más. PERO debe definir el comportamiento de clase en una interfaz (ver más abajo). nótese bien siempre habrá un lugar para pruebas manuales sobre la pirámide de pruebas.

  

¿No deberíamos centrarnos más en las pruebas de integración?

Cada vez más las pruebas de integración tampoco son la respuesta, son costosas de escribir, ejecutar y mantener. Dependiendo de la configuración de su compilación, su administrador de compilación puede excluirlos de todos modos, lo que hace que dependan de un desarrollador que recuerde (¡nunca es bueno!).

He visto a los desarrolladores pasar horas tratando de corregir las pruebas de integración rotas que habrían encontrado en cinco minutos si hubieran tenido buenas pruebas unitarias. Si esto no funciona, intente ejecutar el software, eso es lo único que le importará a sus usuarios finales. No tiene sentido tener millones de pruebas unitarias que pasan si toda la sala de naipes cae cuando el usuario ejecuta toda la suite.

Si desea asegurarse de que la clase A consuma la clase X de la misma manera, debe utilizar una interfaz en lugar de una concreción. Entonces, es más probable que se detecte un cambio importante en el momento de la compilación.

    
respondido por el Robbie Dee 27.03.2018 - 14:24
fuente
9

Eso es correcto.

Las pruebas unitarias están ahí para probar la funcionalidad aislada de una unidad, una comprobación de primera vista de que funciona según lo previsto y no contiene errores estúpidos.

Las pruebas unitarias no están ahí para probar que funciona toda la aplicación.

Lo que mucha gente olvida es que las pruebas unitarias son solo los medios más rápidos y sucios de validar su código. Una vez que sepa que sus pequeñas rutinas funcionan, también tendrá que ejecutar las pruebas de integración. La prueba de unidad por sí sola es solo ligeramente mejor que ninguna prueba.

La razón por la que tenemos pruebas unitarias es que se supone que son baratas. Rápido de crear y ejecutar y mantener. Una vez que comiences a convertirlos en pruebas de integración mínimas, te encontrarás en un mundo de dolor. También puede pasar la prueba de integración completa e ignorar las pruebas unitarias por completo si va a hacer eso.

ahora, algunas personas piensan que una unidad no es solo una función en una clase, sino toda la clase en sí (yo mismo incluido). Sin embargo, todo lo que hace es aumentar el tamaño de la unidad, por lo que es posible que necesite menos pruebas de integración, pero aún así lo necesita. Aún es imposible verificar que su programa haga lo que se supone que debe hacer sin un conjunto completo de pruebas de integración.

y luego, aún deberá ejecutar las pruebas de integración completas en un sistema en vivo (o semi-activo) para verificar que funciona con las condiciones que usa el cliente.

    
respondido por el gbjbaanb 28.03.2018 - 18:34
fuente
2

Las pruebas unitarias no prueban la exactitud de nada. Esto es cierto para todas las pruebas. Por lo general, las pruebas unitarias se combinan con el diseño basado en el contrato (el diseño por contrato es otra forma de decirlo) y, posiblemente, las pruebas de corrección automatizadas, si es necesario verificar la corrección periódicamente.

Si tiene contratos reales, que consisten en invariantes de clase, condiciones previas y condiciones de publicación, es posible probar la corrección jerárquicamente, basando la corrección de los componentes de nivel superior en los contratos de los componentes de nivel inferior. Este es el concepto fundamental detrás del diseño por contrato.

    
respondido por el Frank Hileman 27.03.2018 - 20:33
fuente
2

Encuentro que las pruebas muy burladas rara vez son útiles. La mayoría de las veces, termino reimplementando el comportamiento que ya tiene la clase original, lo que anula totalmente el propósito de burlarse.

M.e. una mejor estrategia es tener una buena separación de inquietudes (por ejemplo, puede probar la Parte A de su aplicación sin llevar las partes B a la Z). Una arquitectura tan buena realmente ayuda a escribir una buena prueba.

Además, estoy más que dispuesto a aceptar efectos secundarios siempre que pueda revertirlos, por ejemplo. Si mi método modifica los datos en la base de datos, ¡déjalo! Mientras pueda devolver el db al estado anterior, ¿cuál es el daño? Además, existe la ventaja de que mi prueba puede verificar si los datos se ven como se esperaba. Los DB en memoria o las versiones de prueba específicas de dbs realmente ayudan aquí (por ejemplo, la versión de prueba en memoria de RavenDB).

Finalmente, me gusta hacer burlas en los límites del servicio, por ejemplo, no haga esa llamada http al servicio b, pero interceptémoslo e introduzcamos un apropiado

    
respondido por el Christian Sauer 31.03.2018 - 07:19
fuente
1

Desearía que las personas en ambos campos entiendan que las pruebas de clase y las pruebas de comportamiento no son ortogonales.

La prueba de clase y la prueba de unidad se usan indistintamente y quizás no deberían serlo. Algunas pruebas unitarias simplemente se implementan en las clases. Eso es todo. Las pruebas de unidad han ocurrido durante décadas en idiomas sin clases.

En cuanto a los comportamientos de prueba, es perfectamente posible hacer esto dentro de las pruebas de clase utilizando la construcción GWT.

Además, si las pruebas automatizadas avanzan a lo largo de la clase o las líneas de comportamiento dependen de sus prioridades. Algunos necesitarán crear prototipos rápidamente y sacar algo por la puerta, mientras que otros tendrán restricciones de cobertura debido a los estilos internos. Muchas razones. Ambos son enfoques perfectamente válidos. Usted paga su dinero, toma su elección.

Entonces, qué hacer cuando el código se rompe. Si se ha codificado en una interfaz, solo la concreción debe cambiar (junto con cualquier prueba).

Sin embargo, la introducción de un nuevo comportamiento no debe comprometer el sistema en absoluto. Linux y otros están llenos de características obsoletas. Y cosas como los constructores (y los métodos) se pueden sobrecargar sin forzar a que cambien todos los códigos de llamada.

Donde las pruebas de clase ganan es donde necesitas hacer un cambio en una clase que aún no ha sido instalada (debido a limitaciones de tiempo, complejidad o lo que sea). Es mucho más fácil comenzar con una clase si tiene exámenes completos.

    
respondido por el Belgian Dog 28.03.2018 - 19:42
fuente
0

A menos que la interfaz para X cambie, no necesita cambiar la prueba de la unidad para A porque nada relacionado con A ha cambiado. Parece que realmente escribiste una prueba de unidad de X y A juntas, pero la llamaste prueba de unidad de A:

  

Cuando escribes pruebas unitarias para A, te burlas de X. En otras palabras, mientras pruebas la unidad A, configuras (postulas) el comportamiento de la simulación de X como X1.

Idealmente, el simulacro de X debería simular todos los comportamientos posibles de X, no solo el comportamiento que espera de X. Así que no importa lo que realmente implementes en X, A ya debería ser capaz de manejarlo. Por lo tanto, ningún cambio en X, aparte de cambiar la interfaz en sí, tendrá ningún efecto en la prueba de la unidad para A.

Por ejemplo: supongamos que A es un algoritmo de clasificación, y A proporciona datos para clasificar. El simulacro de X debe proporcionar un valor de retorno nulo, una lista vacía de datos, un elemento único, múltiples elementos ya ordenados, múltiples elementos aún no ordenados, múltiples elementos ordenados hacia atrás, listas con el mismo elemento repetido, valores nulos entremezclados, ridículamente gran cantidad de elementos, y también debería lanzar una excepción.

Entonces quizás X inicialmente devolvió los datos ordenados los lunes y las listas vacías los martes. Pero ahora, cuando X devuelve datos sin clasificar los lunes y lanza excepciones los martes, a A no le importa, esos escenarios ya estaban cubiertos en la prueba de unidad de A.

    
respondido por el Moby Disk 28.03.2018 - 16:50
fuente
-2

Tienes que mirar diferentes pruebas.

Las pruebas unitarias en sí mismas solo probarán X. Están allí para evitar que cambies el comportamiento de X pero no aseguran todo el sistema. Se aseguran de que usted pueda refactorizar su clase sin introducir un cambio en el comportamiento. Y si rompes X, lo rompes ...

A debería simular X para sus pruebas de unidad y la prueba con Mock debe seguir pasando incluso después de que lo cambies.

¡Pero hay más de un nivel de prueba! También hay pruebas de integración. Estas pruebas están ahí para validar la interacción entre las clases. Estas pruebas usualmente tienen un precio más alto ya que no usan simulacros para todo. Por ejemplo, una prueba de integración podría escribir un registro en una base de datos, y una prueba de unidad no debería tener dependencias externas.

Además, si X necesita tener un nuevo comportamiento, sería mejor proporcionar un nuevo método que proporcione el resultado deseado

    
respondido por el Heiko Hatzfeld 29.03.2018 - 10:08
fuente

Lea otras preguntas en las etiquetas