Tiene una clase X y escribe algunas pruebas unitarias que verifican el comportamiento X1. También hay una clase A que toma X como una dependencia.
Cuando escribe pruebas unitarias para A, se burla de X. En otras palabras, mientras prueba unitariamente A, establece (postula) que el comportamiento del simulacro de X sea X1. El tiempo pasa, la gente usa su sistema, necesita cambios, X evoluciona: usted modifica X para mostrar el comportamiento X2. Obviamente, las pruebas unitarias para X fallarán y deberá 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)?
Espero respuestas en la línea de: "Ese no es el propósito de las pruebas unitarias", pero ¿qué valor tienen entonces las pruebas unitarias? ¿Realmente solo te dice que cuando pasan todas las pruebas, no has introducido un cambio radical? 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?
fuente
X1
que está diciendo queX
implementa la interfazX1
. Si cambia la interfazX1
alX2
simulacro que usó en las otras pruebas, ya no debería compilar, por lo tanto, se ve obligado a corregir esas pruebas también. Los cambios en el comportamiento de la clase no deberían importar. De hecho, su claseA
no debería depender de los detalles de implementación (que es lo que estaría cambiando en ese caso). Por lo tanto, las pruebas unitariasA
todavía son correctas y le dicen queA
funciona dada una implementación ideal de la interfaz.Respuestas:
¿Vos si? No lo hago, a menos que sea absolutamente necesario. Tengo que si:
X
es lento oX
tiene efectos secundariosSi ninguno de estos aplica, entonces mi unidad de
A
pruebasX
también lo hará . 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 estaría de acuerdo: ¿cuál es el punto de tales pruebas unitarias? Entonces 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 con usted llamando a estas pruebas, "pruebas unitarias", simplemente llámelas "pruebas automatizadas" y continúe escribiendo buenas pruebas automatizadas.
fuente
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 depender solo de las pruebas de integración es la explosión combinatoria resultante de las interacciones entre todas sus unidades.
Digamos que tiene clase A, que requiere 10 pruebas unitarias para cubrir completamente todas las rutas. Luego tiene otra clase B, que también requiere 10 pruebas unitarias para cubrir todas las rutas que el código puede recorrer. Ahora, digamos 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 necesita 20 pruebas unitarias + 1 prueba de integración para cubrir completamente todos los casos.
Con las pruebas de integración, necesitará 100 pruebas para cubrir todas las rutas de código.
Aquí hay un muy buen video sobre las desventajas de confiar solo en las pruebas de integración Las pruebas integradas de JB Rainsberger son una estafa HD
fuente
Woah, espera un momento. Las implicaciones de las pruebas para la falla de X son demasiado importantes para pasar por alto de esa manera.
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 hacia atrás en el contrato X.
X2 no es una X, en el sentido de Liskov , por lo que debe pensar en otras formas de satisfacer las necesidades de sus partes interesadas (como la introducción de una nueva especificación Y, implementada por X2).
Para obtener información más profunda, vea Pieter Hinjens: El fin de las versiones de software , o Rich Hickey Simple Made Easy .
Desde la perspectiva de A, existe una condición previa de que el colaborador respete el contrato X. Y su observación es efectivamente que la prueba aislada para A no le garantiza que A reconozca a los colaboradores que violen el contrato X.
Revisar las pruebas integradas son una estafa ; en un nivel alto, se espera que tenga tantas pruebas aisladas como necesite para asegurarse de que X2 implemente el contrato X correctamente, y tantas pruebas aisladas como sea necesario para asegurarse de que A haga lo correcto con 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.
Algunas veces verá esta distinción expresada como pruebas solitarias versus
sociable
pruebas; ver Jay Fields trabajando eficazmente con pruebas unitarias .Una vez más, ver que las pruebas integradas son una estafa: Rainsberger describe en detalle un ciclo de retroalimentación positiva que es común (en su experiencia) a proyectos que dependen de pruebas integradas (ortografía de notas). En resumen, sin las pruebas aisladas / solitarias que aplican presión al diseño , la calidad se degrada, lo que lleva a más errores y pruebas más integradas ...
También necesitará (algunas) pruebas de integración. Además de la complejidad introducida por múltiples módulos, la ejecución de estas pruebas tiende a tener más resistencia que las pruebas aisladas; es más eficiente iterar en verificaciones muy rápidas cuando el trabajo está en progreso, guardando las verificaciones adicionales para cuando cree que está "listo".
fuente
Permítanme comenzar diciendo que la premisa central de la pregunta es errónea.
Nunca está probando (o burlándose) implementaciones, está probando (y burlándose) interfaces .
Si tengo una clase X real que implementa la interfaz X1, puedo escribir un XM simulado que también cumpla con X1. Entonces mi clase A debe usar algo que implemente X1, que puede ser clase X o simulacro XM.
Ahora, supongamos que cambiamos X para implementar una nueva interfaz X2. Bueno, obviamente mi código ya no se compila. A requiere algo que implemente X1 y que ya no exista. El problema ha sido identificado y se puede solucionar.
Supongamos que en lugar de reemplazar X1, simplemente lo modificamos. Ahora la clase A está lista. Sin embargo, el simulador XM ya no implementa la interfaz X1. El problema ha sido identificado y se puede solucionar.
Toda la base para las pruebas unitarias y la burla es que usted escribe código que usa interfaces. Al consumidor de una interfaz no le importa cómo se implementa el código, solo que se cumple el mismo contrato (entradas / salidas).
Esto se rompe cuando sus métodos tienen efectos secundarios, pero creo que se puede excluir de forma segura ya que "no se puede probar ni burlar".
fuente
Tomando sus preguntas por turno:
Son baratos de escribir y ejecutar y obtienes comentarios tempranos. Si rompe X, descubrirá más o menos inmediatamente si tiene buenas pruebas. Ni siquiera considere escribir pruebas de integración a menos que haya probado todas sus capas (sí, incluso en la base de datos).
Tener pruebas que pasan realmente podría decir muy poco. Es posible que no haya 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 pasan. Por lo tanto, el rojo es el primer paso a menudo ignorado del refactor rojo, verde.
Más pruebas, aunque las herramientas son cada vez mejores. PERO debe definir el comportamiento de la clase en una interfaz (ver más abajo). NB siempre habrá un lugar para las pruebas manuales en la cima de la pirámide de pruebas.
Cada vez más pruebas de integración tampoco son la respuesta, son caras de escribir, ejecutar y mantener. Dependiendo de su configuración de compilación, su administrador de compilación puede excluirlos de todos modos, haciéndolos depender de un desarrollador que lo recuerde (¡nunca es algo bueno!).
He visto a los desarrolladores pasar horas tratando de arreglar pruebas de integración rotas que habrían encontrado en cinco minutos si tuvieran buenas pruebas unitarias. De lo contrario, intente simplemente ejecutar el software; eso es todo lo que le interesará a sus usuarios finales. No tiene sentido tener millones de pruebas unitarias que pasan si toda la casa de cartas se 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 usar una interfaz en lugar de una concreción. Entonces, es más probable que se produzca un cambio importante en el momento de la compilación.
fuente
Eso es correcto.
Las pruebas unitarias están ahí para probar la funcionalidad aislada de una unidad, una comprobación a 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 toda la aplicación funciona.
Lo que mucha gente olvida es que las pruebas unitarias son el medio más rápido y sucio de validar su código. Una vez que sepa que sus pequeñas rutinas funcionan, también debe ejecutar las pruebas de integración. Las pruebas unitarias en sí mismas son solo marginalmente mejores que ninguna prueba.
La razón por la que tenemos pruebas unitarias es que se supone que son baratas. Rápido para crear, ejecutar y mantener. Una vez que comienzas a convertirlos en pruebas de integración mínimas, te encuentras en un mundo de dolor. También podría realizar la prueba de integración completa e ignorar por completo las pruebas unitarias 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í (incluido yo mismo). Sin embargo, todo esto hace que aumente el tamaño de la unidad, por lo que es posible que necesite menos pruebas de integración, pero aún así lo necesita. Todavía 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 la prueba de integración completa en un sistema en vivo (o semi-vivo) para verificar que funcione con las condiciones que utiliza el cliente.
fuente
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 un diseño basado en contratos (el diseño por contrato es otra forma de decirlo) y posiblemente pruebas de corrección automatizadas, si la corrección necesita ser verificada regularmente.
Si tiene contratos reales, que consisten en invariantes de clase, condiciones previas y condiciones de publicación, es posible demostrar 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.
fuente
fac(5) == 120
, usted ha no probado quefac()
en efecto devolver el factorial de su argumento. Solo ha demostrado quefac()
devuelve el factorial de cinco cuando pasa5
. E incluso eso no es seguro, yafac()
que posiblemente podría regresar42
en su lugar los primeros lunes después de un eclipse total en Tombuctú ... El problema aquí es que no puede probar el cumplimiento verificando las entradas de prueba individuales, necesitaría verificar todas las entradas posibles, y también demuestre que no ha olvidado ninguno (como leer el reloj del sistema).Encuentro que las pruebas muy burladas rara vez son útiles. La mayoría de las veces, termino reimplementando el comportamiento que la clase original ya tiene, lo que anula totalmente el propósito de burlarse.
Para mí, una mejor estrategia es tener una buena separación de preocupaciones (por ejemplo, puede probar la Parte A de su aplicación sin incluir las partes B a Z). Una arquitectura tan buena realmente ayuda a escribir una buena prueba.
Además, estoy más que dispuesto a aceptar los efectos secundarios siempre que pueda revertirlos, por ejemplo, si mi método modifica los datos en la base de datos, ¡déjelo! Mientras pueda hacer que la base de datos vuelva al estado anterior, ¿cuál es el daño? Además, existe el beneficio de que mi prueba puede verificar si los datos se ven como se esperaba. Las bases de datos 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 interceptemos e introduzcamos un apropiado
fuente
Desearía que las personas en ambos campos entiendan que las pruebas de clase y las pruebas de comportamiento no son ortogonales.
Las pruebas de clase y las pruebas unitarias se usan indistintamente y tal vez no deberían usarse. Algunas pruebas unitarias simplemente se implementan en clases. Eso es todo. Las pruebas unitarias se han realizado durante décadas en idiomas sin clases.
En cuanto a los comportamientos de prueba, es perfectamente posible hacerlo dentro de las pruebas de clase utilizando la construcción GWT.
Además, si sus pruebas automatizadas proceden a lo largo de las líneas de clase o comportamiento, más bien depende de sus prioridades. Algunos necesitarán hacer prototipos rápidamente y sacar algo por la puerta, mientras que otros tendrán restricciones de cobertura debido a los estilos de la casa. Muchas razones. Ambos son enfoques perfectamente válidos. Paga su dinero, toma su elección.
Entonces, qué hacer cuando el código se rompe. Si se ha codificado a una interfaz, solo la concreción debe cambiar (junto con cualquier prueba).
Sin embargo, la introducción de un nuevo comportamiento no tiene por qué 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 todos los códigos de llamada a cambiar.
Donde las pruebas de clase ganan es donde necesita hacer un cambio en una clase que aún no se ha conectado (debido a limitaciones de tiempo, complejidad o lo que sea). Es mucho más fácil comenzar con una clase si tiene pruebas exhaustivas.
fuente
A menos que la interfaz para X haya cambiado, no necesita cambiar la prueba de unidad para A porque nada relacionado con A ha cambiado. Parece que realmente escribiste una prueba unitaria de X y A juntos, pero la llamaste prueba unitaria de A:
Idealmente, la simulación de X debería simular todos los comportamientos posibles de X, no solo el comportamiento que espera de X. Entonces, sin importar lo que implemente realmente 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 unitaria de A.
Por ejemplo: supongamos que A es un algoritmo de clasificación y A proporciona datos para ordenar. La simulación de X debe proporcionar un valor de retorno nulo, una lista vacía de datos, un solo elemento, varios elementos ya ordenados, varios elementos aún no ordenados, múltiples elementos ordenados al revés, listas con el mismo elemento repetido, valores nulos entremezclados, ridículamente gran cantidad de elementos, y también debería arrojar una excepción.
Entonces, tal vez X inicialmente devolvió datos ordenados los lunes y listas vacías los martes. Pero ahora, cuando X devuelve datos sin clasificar los lunes y arroja excepciones los martes, a A no le importa, esos escenarios ya estaban cubiertos en la prueba de la unidad de A.
fuente
Tienes que mirar diferentes pruebas.
Las pruebas unitarias por sí mismas solo probarán X. Están allí para evitar que cambie el comportamiento de X pero no asegura todo el sistema. Se aseguran de que pueda refactorizar su clase sin introducir un cambio en el comportamiento. Y si rompes X, lo rompiste ...
De hecho, A debe simular X para sus pruebas unitarias y la prueba con el simulacro debe seguir aprobando incluso después de cambiarlo.
¡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 generalmente tienen un precio más alto ya que no usan simulacros para todo. Por ejemplo, una prueba de integración en realidad podría escribir un registro en una base de datos, y una prueba unitaria 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
fuente