(¿Por qué) es importante que una prueba unitaria no pruebe dependencias?

100

Comprendo el valor de las pruebas automatizadas y las uso donde sea que el problema esté lo suficientemente específico como para que pueda encontrar buenos casos de prueba. Sin embargo, me he dado cuenta de que algunas personas aquí y en StackOverflow hacen hincapié en probar solo una unidad, no sus dependencias. Aquí no consigo ver el beneficio.

La burla / aplastamiento para evitar las dependencias de prueba agrega complejidad a las pruebas. Agrega flexibilidad artificial / requisitos de desacoplamiento a su código de producción para soportar el simulacro. (No estoy de acuerdo con nadie que diga que esto promueve un buen diseño. Escribir código adicional, introducir elementos 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 sobreingeniería, no buen diseño.)

En segundo lugar, las dependencias de prueba significan que el código crítico de bajo nivel que se usa en todas partes se prueba con entradas distintas a las que los que escribieron sus pruebas pensaron explícitamente. He encontrado muchos errores en la funcionalidad de bajo nivel al ejecutar pruebas unitarias en la funcionalidad de alto nivel sin burlarme de la funcionalidad de bajo nivel de la que dependía. Lo ideal sería que las pruebas unitarias las encontraran para la funcionalidad de bajo nivel, pero los casos perdidos siempre suceden.

¿Cuál es el otro lado de esto? ¿Es realmente importante que una prueba de unidad 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 ninguna dependencia externa directa.

    
pregunta dsimcha 06.04.2011 - 03:05

11 respuestas

113

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 una suite de prueba de integración. La diferencia es que la suite de prueba de integración puede ejecutarse en un marco de prueba diferente y probablemente no como parte de la compilación porque lleva más tiempo.

Para nuestro producto: Nuestras pruebas unitarias se ejecutan con cada compilación, tomando segundos. Un subconjunto de nuestras pruebas de integración se ejecuta con cada registro, en 10 minutos. Nuestra suite de integración completa se ejecuta cada noche, en 4 horas.

    
respondido por el Jeffrey Faust 06.04.2011 - 03:11
37

Las pruebas con todas las dependencias en su lugar siguen siendo importantes, pero es 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 que pasa realmente significa que las cosas están bien y que una prueba que falla realmente significa un problema en el código de producción, sus pruebas no son tan útiles como pueden ser.

Para que tus pruebas sean confiables, debes hacer algunas cosas, pero me centraré en una sola 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 hay una configuración extensa o Configuración necesaria para hacerlos ir. Idealmente, cualquier persona debería poder verificar la última versión del código, ejecutar las pruebas de inmediato y verlas pasar.

Retirar las dependencias de otras cosas (sistema de archivos, base de datos, servicios web, etc.) le permite evitar la configuración y hace que usted y otros desarrolladores sean menos susceptibles a las situaciones en las que está tentado a decir "Oh, las pruebas fallaron porque No tengo configurada la red compartida. Oh, bueno. Los ejecutaré más tarde ".

Si desea probar lo que hace con algunos datos, su unidad de pruebas para ese código de lógica de negocios no debería preocuparse por cómo obtener esos datos. Ser capaz de probar la lógica central de su aplicación sin depender de elementos de soporte como bases de datos es increíble. Si no lo estás haciendo, te lo estás perdiendo.

P.S. Debo añadir que definitivamente es posible realizar una revisión excesiva en nombre de la capacidad de prueba. La prueba de manejo de 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é hago esto?" mientras se desarrolla.

En cuanto a las dependencias internas, las cosas se ponen un poco embarradas. La forma en que me gusta pensar es que quiero proteger a mi clase tanto como sea posible de cambiar por las razones incorrectas. Si tengo una configuración como esta ...
public class MyClass 
{
    private SomeClass someClass;
    public MyClass()
    {
        someClass = new SomeClass();
    }

    // use someClass in some way
}

Por lo general, no me importa cómo se crea SomeClass . 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 adaptarse a eso.

Ahora, eso es solo tocar en 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 no introdujo en algún momento 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 se basan en el resultado de algún método en SomeClass. Sin burlarse / apagar SomeClass, podría no tener manera de variar esa entrada en la demanda. Si tengo suerte, podría ser capaz de componer mi entorno dentro de la prueba de tal manera que provoque la respuesta correcta de SomeClass, pero esa manera introduce complejidad en mis pruebas y las hace quebradizas.

Volver a escribir 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 simulacros o con un simulacro manual). Generalmente no tengo tengo para introducir una interfaz en este caso. Si hacerlo o no es en muchos sentidos una elección personal que puede ser dictada por el idioma de su elección (por ejemplo, las interfaces son más probables en C #, pero definitivamente no necesitaría una en Ruby).

    
respondido por el Adam Lear 06.04.2011 - 03:21
21

Aparte del problema de la prueba de unidad vs integración, considere lo siguiente.

El widget de clase tiene dependencias en las clases Thingamajig y WhatsIt.

Una prueba de unidad para Widget falla.

¿En qué clase se encuentra el problema?

Si respondió "arrancar el depurador" o "leer el código hasta que lo encuentre", comprende la importancia de probar solo la unidad, no las dependencias.

    
respondido por el Brook 06.04.2011 - 03:28
14

Imagina que la programación es como cocinar. Entonces la prueba de unidad es lo mismo que asegurarse de que sus ingredientes sean frescos, sabrosos, etc. Mientras que la prueba de integración es 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, trabajo, tendrán 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 en funcionamiento. Hago hincapié en "más probable", en lugar de "cierto". Aún necesita sus pruebas de integración, al igual que necesita que alguien pruebe la comida que preparó y le dice que el producto final es bueno. Le será más fácil llegar con ingredientes frescos, eso es todo.

    
respondido por el Julio 06.04.2011 - 04:26
6

Probar las unidades al aislarlas completamente permitirá probar TODAS las posibles variaciones de datos y situaciones en que se puede enviar esta unidad. Debido a que 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 te permitirán probar escenarios específicos que podrían no haberse probado en las pruebas unitarias.

Ambos son importantes. Solo haciendo la prueba unitaria, invariablemente se producirá un error sutil más complejo al integrar los componentes. Solo 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 mejor prueba completa simplemente haciendo la prueba de integración es casi imposible, ya que cuantos más componentes agregue, más se incrementará la combinación de entradas MUY rápido (piense en factorial) y crear una prueba con suficiente cobertura se volverá muy rápidamente imposible.

En resumen, los tres niveles de "pruebas unitarias" que normalmente uso en casi todos mis proyectos:

  • Pruebas unitarias, aisladas con dependencias simuladas o aplastadas para probar el infierno de un solo componente. Lo ideal sería intentar una cobertura completa.
  • Pruebas de integración para probar los errores más sutiles. Controles puntuales cuidadosamente diseñados que mostrarían casos límite y condiciones típicas. La cobertura completa a menudo no es posible, el esfuerzo se centró en lo que haría que el sistema fallara.
  • Pruebas de especificación para inyectar diversos datos de la vida real en el sistema, instrumentar los datos (si es posible) y observar el resultado para garantizar que cumple con las especificaciones y las reglas comerciales.

La inyección de dependencia es un medio muy eficaz para lograr esto, ya que permite aislar componentes para pruebas unitarias con mucha facilidad sin agregar complejidad al sistema. Es posible que su escenario empresarial no justifique 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.

    
respondido por el Newtopian 06.04.2011 - 04:00
4
  

¿Cuál es el otro lado de esto? ¿Es realmente importante que una prueba de unidad no pruebe también sus dependencias? Si es así, ¿por qué?

Unidad. Significa singular.

Probar 2 cosas significa que tienes las dos cosas y todas las dependencias funcionales.

Si agrega una tercera cosa, aumenta 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 los elementos n que se están probando.

Esa es la gran razón.

La simplicidad tiene valor.

    
respondido por el S.Lott 06.04.2011 - 03:10
2

¿Recuerdas cómo aprendiste a hacer recursión? Mi profesor dijo "Supongamos que tienes un método que hace x" (por ejemplo, resuelve fibbonacci para cualquier x). "Para resolver para x tienes que llamar a ese método para x-1 y x-2". De la misma manera, el aplastamiento de dependencias le permite simular que existen y probar que la unidad actual hace lo que debe hacer. El supuesto es, por supuesto, que está probando las dependencias de forma rigurosa.

Esto es, en esencia, el SRP en funcionamiento. Centrándose en una sola responsabilidad, incluso para sus exámenes, elimina la cantidad de malabares mentales que tiene que hacer.

    
respondido por el Michael Brown 06.04.2011 - 06:11
2

(Esta es una respuesta menor. Gracias a @TimWilliscroft por la sugerencia).

Los fallos son más fáciles de localizar si:

  • Tanto la unidad bajo prueba como cada una de sus dependencias se prueban de forma independiente.
  • Cada prueba falla si y solo si hay un error en el código cubierto por la prueba.

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 identificar la ubicación de la falla.

    
respondido por el rwong 06.04.2011 - 09:16
2

Muchas buenas respuestas. También agregaría un par de otros puntos:

La prueba de unidad también le permite probar su código cuando las dependencias no existen . P.ej. usted o su equipo aún no han escrito las otras capas, o tal vez está esperando en una interfaz entregada por otra compañía.

Las pruebas unitarias también significan que no tienes que tener un entorno completo en tu máquina dev (por ejemplo, una base de datos, un servidor web, etc.). Recomendaría encarecidamente que todos los desarrolladores tengan un entorno de este tipo, por muy reducido que sea, para corregir errores, etc. Sin embargo, si por alguna razón no es posible imitar el entorno de producción, entonces la unidad al menos las pruebas le brindan cierto nivel de confianza en su código antes de que ingrese a un sistema de prueba más amplio.

    
respondido por el Steve Haigh 06.04.2011 - 10:32
0

Sobre los aspectos de diseño: creo que incluso los proyectos pequeños se benefician al hacer que el código sea verificable. No necesariamente tiene que introducir algo como Guice (una clase de fábrica simple lo hará a menudo), pero separar el proceso de construcción de la lógica de programación da como resultado

  • documentar claramente las dependencias de cada clase a través de su interfaz (muy útil para las personas que son nuevas en el equipo)
  • las clases se vuelven mucho más claras y fáciles de mantener (una vez que se coloca la creación del gráfico de objetos feos en una clase separada)
  • acoplamiento suelto (haciendo los cambios mucho más fáciles)
respondido por el Oliver Weiler 06.04.2011 - 09:45
0

Uh ... hay buenos puntos en las pruebas de unidad e integración escritas en estas respuestas aquí

Echo de menos las vistas prácticas y relacionadas con los costos aquí. Dicho esto, veo claramente el beneficio de las pruebas de unidades atómicas muy aisladas (tal vez muy independientes entre sí y con la opción de ejecutarlas en paralelo y sin ninguna dependencia, como bases de datos, sistemas 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 que piense en "cómo probar" según mi experiencia ... .

¿Mi cliente paga (implícitamente) la cantidad adicional de escritura y el mantenimiento de las pruebas? ¿Un enfoque más basado en pruebas (las pruebas de escritura primero antes de escribir el código) es realmente rentable en mi entorno (análisis de riesgo / costo de fallas de código, personas, especificaciones de diseño, configuración de 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 en gran medida de la calidad de su código (estándares) o los marcos, el IDE, los principios de diseño, etc. que usted y su equipo siguen y cuán experimentados son. Un código bien escrito, fácilmente comprensible, suficientemente bien documentado (idealmente autodocumentado), modular, ... introduce probablemente menos errores que lo contrario. Por lo tanto, la "necesidad" real, la presión o el mantenimiento general / los costos / riesgos de corrección de errores para las pruebas exhaustivas pueden no ser altos.

Vamos a llevarlo al extremo donde sugirió un compañero de trabajo en mi equipo, tenemos que intentar probar en unidad nuestro código de capa de modelo Java EE puro con un 100% de cobertura deseada para todas las clases dentro de la base de datos. O el administrador que desea que las pruebas de integración se cubran con el 100% de todos los posibles casos de uso del mundo real y flujos de trabajo de la interfaz de usuario web porque no queremos arriesgar ningún caso de uso para que falle. Pero tenemos un presupuesto ajustado de alrededor de 1 millón de euros, un plan bastante estricto para codificar todo. Un entorno de cliente, donde los posibles errores de aplicaciones 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 de clientes clave 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! (Solo tenemos una base de datos designada, fácil de probar clones para cada desarrollador y estrechamente acoplados a la aplicación web o la capa del modelo)

Yo mismo trato de escribir la prueba si es posible y desarrollarla mientras pruebo mi código. Pero a menudo lo hago desde un enfoque de arriba hacia abajo (pruebas de integración) y trato de encontrar el punto correcto, donde hacer el "corte de capa de aplicación" para las pruebas importantes (a menudo en la capa de modelo). (porque a menudo es mucho sobre las "capas")

Además, el código de prueba de unidad e integración no viene sin un impacto negativo en tiempo, dinero, mantenimiento, 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 un lote. de código de prueba desarrollado.

Por lo tanto, diría que realmente depende mucho de la confianza y la evaluación de costo / riesgo / beneficio ... como en la vida real donde no puede y no quiere correr con un Lote o 100% mecanismos de seguridad.

El niño puede y debe trepar a alguna parte y puede caerse y lastimarse. El auto 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 no quiero, nunca, conducir en la carretera y tener el volante extraíble en mis manos :)

    
respondido por el Andreas Dietrich 16.01.2018 - 09:51

Lea otras preguntas en las etiquetas