Entiendo el valor de las pruebas automatizadas y lo uso siempre que el problema esté lo suficientemente bien especificado como para poder encontrar buenos casos de prueba. Sin embargo, he notado que algunas personas aquí y en StackOverflow enfatizan probar solo una unidad, no sus dependencias. Aquí no veo el beneficio.
Mockear / stubbing para evitar probar dependencias agrega complejidad a las pruebas. Agrega flexibilidad artificial / requisitos de desacoplamiento a su código de producción para soportar la burla. (No estoy de acuerdo con nadie que diga que esto promueve un buen diseño. Escribir código adicional, introducir cosas como marcos de inyección de dependencias o agregar complejidad a su base de código para hacer que las cosas sean más flexibles / conectables / extensibles / desacopladas sin un caso de uso real es una ingeniería excesiva, no buen diseño.)
En segundo lugar, probar dependencias significa que el código crítico de bajo nivel que se usa en todas partes se prueba con entradas que no sean aquellas en las que quien escribió sus pruebas pensó explícitamente. He encontrado muchos errores en la funcionalidad de bajo nivel al ejecutar pruebas unitarias de funcionalidad de alto nivel sin burlarnos de la funcionalidad de bajo nivel de la que dependía. Lo ideal sería que las pruebas unitarias encontraran la funcionalidad de bajo nivel, pero siempre ocurren casos perdidos.
¿Cuál es el otro lado de esto? ¿Es realmente importante que una prueba unitaria no pruebe también sus dependencias? Si es así, ¿por qué?
Editar: puedo entender el valor de burlarse de dependencias externas como bases de datos, redes, servicios web, etc. (Gracias a Anna Lear por motivarme para aclarar esto). Me refería a dependencias internas , es decir, otras clases, funciones estáticas, etc. que no tienen dependencias externas directas.
Respuestas:
Es una cuestión de definición. Una prueba con dependencias es una prueba de integración, no una prueba unitaria. También debe tener un conjunto de pruebas de integración. La diferencia es que el conjunto de pruebas de integración se puede ejecutar en un marco de prueba diferente y probablemente no como parte de la compilación porque demoran más.
Para nuestro producto: nuestras pruebas unitarias se ejecutan con cada compilación, lo que lleva segundos. Un subconjunto de nuestras pruebas de integración se ejecuta con cada registro, lo que lleva 10 minutos. Nuestra suite de integración completa se ejecuta todas las noches y demora 4 horas.
fuente
La prueba con todas las dependencias en su lugar sigue siendo importante, pero está más en el ámbito de las pruebas de integración como dijo Jeffrey Faust.
Uno de los aspectos más importantes de las pruebas unitarias es hacer que sus pruebas sean confiables. Si no confía en que una prueba aprobada realmente significa que las cosas están bien y que una prueba reprobada realmente significa un problema en el código de producción, sus pruebas no son tan útiles como pueden ser.
Para que sus pruebas sean confiables, debe hacer algunas cosas, pero me voy a centrar en solo una para esta respuesta. Debe asegurarse de que sean fáciles de ejecutar, de modo que todos los desarrolladores puedan ejecutarlos fácilmente antes de ingresar el código. "Fácil de ejecutar" significa que sus pruebas se ejecutan rápidamente y no se necesita una configuración o configuración extensa para hacerlas funcionar. Idealmente, cualquiera debería poder consultar la última versión del código, ejecutar las pruebas de inmediato y verlas pasar.
Abstraer las dependencias de otras cosas (sistema de archivos, base de datos, servicios web, etc.) le permite evitar la necesidad de configuración y lo hace a usted y a otros desarrolladores menos susceptibles a situaciones en las que se siente tentado a decir "Oh, las pruebas fallaron porque no lo hice" no tengo configurado el recurso compartido de red. Bueno, los ejecutaré más tarde ".
Si desea probar lo que hace con algunos datos, las pruebas unitarias para ese código de lógica de negocios no deberían importarle cómo obtiene esos datos. Poder probar la lógica central de su aplicación sin depender de cosas de soporte como bases de datos es increíble. Si no lo estás haciendo, te lo estás perdiendo.
PD: Debo agregar que definitivamente es posible hacer ingeniería excesiva en nombre de la capacidad de prueba. Probar su aplicación ayuda a mitigar eso. Pero, en cualquier caso, las implementaciones deficientes de un enfoque no hacen que el enfoque sea menos válido. Cualquier cosa puede ser mal utilizada y exagerada si uno no sigue preguntando "¿por qué estoy haciendo esto?" mientras se desarrolla
En cuanto a las dependencias internas, las cosas se ponen un poco turbias. La forma en que me gusta pensar es que quiero proteger a mi clase tanto como sea posible del cambio por las razones equivocadas. Si tengo una configuración como esta ...
Generalmente no me importa cómo
SomeClass
se crea. Solo quiero usarlo. Si SomeClass cambia y ahora requiere parámetros para el constructor ... ese no es mi problema. No debería tener que cambiar MyClass para acomodar eso.Ahora, eso es solo tocar la parte de diseño. En lo que respecta a las pruebas unitarias, también quiero protegerme de otras clases. Si estoy probando MyClass, me gusta saber que no hay dependencias externas, que SomeClass en algún momento no introdujo una conexión de base de datos o algún otro enlace externo.
Pero un problema aún mayor es que también sé que algunos de los resultados de mis métodos dependen de la salida de algún método en SomeClass. Sin burlarme de SomeClass, podría no tener forma de variar esa entrada en la demanda. Si tengo suerte, podría componer mi entorno dentro de la prueba de tal manera que provoque la respuesta correcta de SomeClass, pero ir de esa manera introduce complejidad en mis pruebas y las hace frágiles.
Reescribir MyClass para aceptar una instancia de SomeClass en el constructor me permite crear una instancia falsa de SomeClass que devuelve el valor que quiero (ya sea a través de un marco de imitación o con una simulación manual). Generalmente no tengo que introducir una interfaz en este caso. Si hacerlo o no es, en muchos sentidos, una elección personal que puede ser dictada por su idioma de elección (por ejemplo, las interfaces son más probables en C #, pero definitivamente no necesitaría una en Ruby).
fuente
Aparte del problema de la unidad frente a la prueba de integración, considere lo siguiente.
Class Widget tiene dependencias en las clases Thingamajig y WhatsIt.
Una prueba unitaria para Widget falla.
¿En qué clase se encuentra el problema?
Si respondió "iniciar el depurador" o "leer el código hasta que lo encuentre", comprenderá la importancia de probar solo la unidad, no las dependencias.
fuente
Imagina que programar es como cocinar. Luego, las pruebas unitarias son lo mismo que asegurarse de que sus ingredientes sean frescos, sabrosos, etc. Mientras que las pruebas de integración son como asegurarse de que su comida sea deliciosa.
En última instancia, asegurarse de que su comida sea deliciosa (o que su sistema funcione) es lo más importante, el objetivo final. Pero si sus ingredientes, quiero decir, unidades, funcionan, tendrá una forma más barata de averiguarlo.
De hecho, si puede garantizar que sus unidades / métodos funcionen, es más probable que tenga un sistema que funcione. Destaco "más probable", en lugar de "cierto". Todavía necesita sus pruebas de integración, al igual que necesita que alguien pruebe la comida que cocinó y le diga que el producto final es bueno. Te será más fácil llegar con ingredientes frescos, eso es todo.
fuente
Probar las unidades aislándolas completamente permitirá probar TODAS las posibles variaciones de datos y situaciones a las que se puede enviar esta unidad. Como está aislado del resto, puede ignorar las variaciones que no tienen un efecto directo en la unidad que se va a probar. Esto a su vez disminuirá en gran medida la complejidad de las pruebas.
Las pruebas con varios niveles de dependencias integradas le permitirán probar escenarios específicos que podrían no haberse probado en las pruebas unitarias.
Ambos son importantes Simplemente haciendo una prueba unitaria, invariablemente se producirá un error sutil más complejo al integrar componentes. Simplemente haciendo pruebas de integración significa que está probando un sistema sin la confianza de que las partes individuales de la máquina no han sido probadas. Pensar que puede lograr una prueba completa mejor simplemente haciendo pruebas de integración es casi imposible, ya que a medida que aumenta la cantidad de componentes, la combinación de entradas crece MUY rápido (piense en factorial) y crear una prueba con suficiente cobertura se vuelve muy rápidamente imposible.
En resumen, los tres niveles de "pruebas unitarias" que suelo usar en casi todos mis proyectos:
La inyección de dependencia es un medio muy eficiente para lograr esto, ya que permite aislar componentes para pruebas unitarias muy fácilmente sin agregar complejidad al sistema. Es posible que su escenario empresarial no garantice el uso del mecanismo de inyección, pero su escenario de prueba casi lo hará. Esto para mí es suficiente para que sea indispensable. También puede usarlos para probar los diferentes niveles de abstracción de forma independiente a través de pruebas de integración parcial.
fuente
Unidad. Significa singular.
Probar 2 cosas significa que tiene las dos cosas y todas las dependencias funcionales.
Si agrega una tercera cosa, aumentará las dependencias funcionales en las pruebas más allá de forma lineal. Las interconexiones entre las cosas crecen más rápidamente que la cantidad de cosas.
Hay n (n-1) / 2 dependencias potenciales entre n elementos que se están probando.
Esa es la gran razón.
La simplicidad tiene valor.
fuente
¿Recuerdas cómo aprendiste a hacer recursividad por primera vez? Mi profesor dijo "Suponga que tiene un método que hace x" (por ejemplo, resuelve fibbonacci para cualquier x). "Para resolver x tienes que llamar a ese método para x-1 y x-2". Del mismo modo, eliminar las dependencias le permite fingir que existen y probar que la unidad actual hace lo que debería hacer. Por supuesto, se supone que está probando las dependencias con la misma rigurosidad.
Este es, en esencia, el SRP en el trabajo. Centrarse en una única responsabilidad, incluso para sus exámenes, elimina la cantidad de malabarismo mental que tiene que hacer.
fuente
(Esta es una respuesta menor. Gracias @TimWilliscroft por la pista).
Las fallas son más fáciles de localizar si:
Esto funciona muy bien en papel. Sin embargo, como se ilustra en la descripción de OP (las dependencias tienen errores), si las dependencias no se están probando, sería difícil determinar la ubicación de la falla.
fuente
Muchas buenas respuestas. También agregaría un par de otros puntos:
Las pruebas unitarias también le permiten probar su código cuando sus dependencias no existen . Por ejemplo, usted o su equipo, aún no han escrito las otras capas, o tal vez están esperando en una interfaz entregada por otra compañía.
Las pruebas unitarias también significan que no tiene que tener un entorno completo en su máquina de desarrollo (por ejemplo, una base de datos, un servidor web, etc.). Recomendaría fuerte que todos los desarrolladores hacer tener un entorno de este tipo, sin embargo cortado es, con el fin de errores de reprografía, etc. Sin embargo, si por alguna razón no es posible imitar el entorno de producción a continuación, la unidad de pruebas, al menos, yu da un cierto nivel de confianza en su código antes de que entre en un sistema de prueba más grande.
fuente
Acerca de los aspectos de diseño: creo que incluso los proyectos pequeños se benefician al hacer que el código sea comprobable. No necesariamente tiene que introducir algo como Guice (a menudo lo hará una clase simple de fábrica), pero separar el proceso de construcción de la lógica de programación da como resultado
fuente
Uh ... ¡ hay buenos puntos en las pruebas de unidad e integración escritas en estas respuestas aquí!
Echo de menos las opiniones prácticas y relacionadas con los costos aquí. Dicho esto, veo claramente el beneficio de las pruebas unitarias muy aisladas / atómicas (tal vez altamente independientes entre sí y con la opción de ejecutarlas en paralelo y sin dependencias, como bases de datos, sistema de archivos, etc.) y pruebas de integración (nivel superior), pero ... también es una cuestión de costos (tiempo, dinero, ...) y riesgos .
Por lo tanto, hay otros factores, que son mucho más importantes (como "qué probar" ) antes de pensar en "cómo probar" desde mi experiencia ...
¿Paga mi cliente (implícitamente) la cantidad adicional de escritura y mantenimiento de pruebas? ¿Es un enfoque más basado en pruebas (escribir las pruebas antes de escribir el código) realmente rentable en mi entorno (análisis de riesgo / costo de fallas del código, personas, especificaciones de diseño, configuración del entorno de prueba)? El código siempre tiene errores, pero ¿podría ser más rentable mover las pruebas al uso de producción (en el peor de los casos)?
También depende mucho de la calidad de su código (estándares) o de los marcos, IDE, principios de diseño, etc. que usted y su equipo sigan y cuán experimentados sean. Un código bien escrito, fácilmente comprensible, suficientemente documentado (idealmente autodocumentado), modular, ... introduce probablemente menos errores que lo contrario. Por lo tanto, la "necesidad" real, la presión o los costos / riesgos generales de mantenimiento / corrección de errores para pruebas extensas pueden no ser altos.
Llevemos al extremo donde sugirió un compañero de trabajo de mi equipo, tenemos que intentar probar nuestro código de capa de modelo Java EE puro con una cobertura deseada del 100% para todas las clases dentro y burlándose de la base de datos. O el administrador que desea que las pruebas de integración se cubran con un 100% de todos los posibles casos de uso del mundo real y flujos de trabajo de interfaz de usuario web porque no queremos arriesgarnos a que falle ningún caso de uso. Pero tenemos un presupuesto ajustado de aproximadamente 1 millón de euros, un plan bastante ajustado para codificar todo. Un entorno de cliente, donde los posibles errores de la aplicación no serían un gran peligro para los humanos o las empresas. Nuestra aplicación se probará internamente con (algunas) pruebas unitarias importantes, pruebas de integración, pruebas clave de clientes con planes de prueba diseñados, una fase de prueba, etc. ¡No desarrollamos una aplicación para alguna fábrica nuclear o la producción farmacéutica!
Yo mismo intento escribir una prueba si es posible y desarrollarla mientras la unidad prueba mi código. Pero a menudo lo hago desde un enfoque de arriba hacia abajo (prueba de integración) y trato de encontrar el buen punto, dónde hacer el "corte de la capa de la aplicación" para las pruebas importantes (a menudo en la capa del modelo). (porque a menudo se trata mucho de las "capas")
Además, el código de prueba de unidad e integración no tiene un impacto negativo en el tiempo, el dinero, el mantenimiento, la codificación, etc. Es genial y debe aplicarse, pero con cuidado y teniendo en cuenta las implicaciones al comenzar o después de 5 años de muchas pruebas desarrolladas. código.
Entonces, diría que realmente depende mucho de la evaluación de la confianza y el costo / riesgo / beneficio ... como en la vida real, donde no puedes y no quieres correr con muchos o 100% mecanismos de seguridad.
El niño puede y debe subir a algún lugar y puede caerse y lastimarse. El automóvil puede dejar de funcionar porque llené el combustible incorrecto (entrada no válida :)). La tostada puede quemarse si el botón de tiempo deja de funcionar después de 3 años. Pero, nunca, quiero conducir en la carretera y sostener mi volante separado en mis manos :)
fuente