unit testing - tipos - ¿Cuáles son los pros y los contras de las pruebas unitarias automatizadas frente a las pruebas de integración automatizadas?
tipos de pruebas de software (12)
Aunque la configuración que describiste suena bien, las pruebas unitarias también ofrecen algo importante. Las pruebas unitarias ofrecen niveles finos de granularidad. Con el acoplamiento suelto y la inyección de dependencia, puede probar casi todos los casos importantes. Puedes estar seguro de que las unidades son robustas; puede analizar métodos individuales con puntuaciones de entradas o cosas interesantes que no necesariamente ocurren durante sus pruebas de integración.
Por ejemplo, si desea ver de manera determinista cómo una clase manejará algún tipo de falla que requeriría una configuración complicada (por ejemplo, una excepción de red al recuperar algo de un servidor), puede escribir fácilmente su propia clase de conexión de doble red de prueba, inyectarla y decírselo. Para lanzar una excepción siempre que lo desee. A continuación, puede asegurarse de que la clase bajo prueba maneja con gracia la excepción y continúa en un estado válido.
Recientemente hemos estado agregando pruebas automatizadas a nuestras aplicaciones Java existentes.
Que tenemos
La mayoría de estas pruebas son pruebas de integración, que pueden cubrir una pila de llamadas como:
- HTTP post en un servlet
- El servlet valida la solicitud y llama a la capa de negocios.
- La capa de negocios hace un montón de cosas a través de hibernación, etc. y actualiza algunas tablas de base de datos
- El servlet genera algunos XML, lo ejecuta a través de XSLT para producir respuestas HTML.
Luego verificamos que el servlet respondió con el XML correcto y que existen las filas correctas en la base de datos (nuestra instancia de Oracle de desarrollo). Estas filas son luego eliminadas.
También tenemos algunas pruebas unitarias más pequeñas que verifican las llamadas de un solo método.
Estas pruebas se ejecutan como parte de nuestras compilaciones nocturnas (o adhoc).
La pregunta
Esto parece bueno porque estamos verificando los límites de nuestro sistema: solicitud / respuesta de servlet en un extremo y base de datos en el otro. Si esto funciona, entonces somos libres de refactorizar o desordenar con cualquier cosa intermedia y tenemos cierta confianza en que el servlet bajo prueba continúa funcionando.
¿Con qué problemas es probable que nos encontremos con este enfoque?
No puedo ver cómo ayudaría agregar un montón más de pruebas de unidad en clases individuales. ¿No sería eso más difícil refactorizar ya que es mucho más probable que tengamos que tirar y reescribir las pruebas?
Como muchos han mencionado, las pruebas de integración le dirán si su sistema funciona, y las pruebas unitarias le indicarán dónde no. Estrictamente desde una perspectiva de prueba, estos dos tipos de pruebas se complementan entre sí.
No puedo ver cómo ayudaría agregar un montón más de pruebas de unidad en clases individuales. ¿No sería eso más difícil refactorizar ya que es mucho más probable que tengamos que tirar y reescribir las pruebas?
No. Facilitará y perfeccionará la refactorización y aclarará qué refactorizaciones son adecuadas y relevantes. Por eso decimos que TDD se trata de diseño, no de pruebas. Es bastante común para mí escribir una prueba para un método y al averiguar cómo expresar el resultado de ese método debería ser una implementación muy simple en términos de algún otro método de la clase bajo prueba. Esa implementación frecuentemente encuentra su camino en la clase bajo prueba. Implementaciones más sencillas, sólidas, límites más limpios, métodos más pequeños: TDD (pruebas de unidad), específicamente: lo guían en esta dirección, y las pruebas de integración no lo hacen. Ambos son importantes, ambos útiles, pero tienen diferentes propósitos.
Sí, puede encontrarse modificando y eliminando las pruebas unitarias en ocasiones para adaptarse a refactorizaciones; Eso está bien, pero no es difícil. Y realizar esas pruebas unitarias, y pasar por la experiencia de escribirlas, le brinda una mejor comprensión de su código y un mejor diseño.
Está preguntando los pros y los contras de dos cosas diferentes (¿cuáles son los pros y los contras de montar a caballo frente a una motocicleta?)
Por supuesto, ambas son "pruebas automáticas" (~ conducción), pero eso no significa que sean alternativas (usted no monta un caballo por cientos de millas, y no monta una motocicleta en un fangoso cerrado para vehículos). lugares)
Las pruebas unitarias prueban la unidad más pequeña del código, generalmente un método. Cada prueba de unidad está estrechamente relacionada con el método que está probando, y si está bien escrita, está vinculada (casi) solo con eso.
Son excelentes para guiar el diseño de un nuevo código y la refactorización del código existente. Son excelentes para detectar problemas mucho antes de que el sistema esté listo para las pruebas de integración. Tenga en cuenta que escribí una guía y todo el desarrollo guiado por pruebas es sobre esta palabra.
No tiene sentido tener pruebas unitarias manuales.
¿Qué pasa con la refactorización, que parece ser tu principal preocupación? Si está refactorizando solo la implementación (contenido) de un método, pero no su existencia o "comportamiento externo", la prueba de unidad sigue siendo válida e increíblemente útil (no puede imaginar cuánta utilidad hasta que lo intente).
Si está refactorizando de forma más agresiva, cambiando la existencia o el comportamiento de los métodos, entonces sí, debe escribir una nueva prueba de unidad para cada nuevo método, y posiblemente desechar la anterior. Pero escribir la prueba de unidad, especialmente si lo escribe antes del código, ayudará a aclarar el diseño (es decir, qué debería hacer el método y qué no debería) sin que los detalles de la implementación lo confundan (es decir, cómo debería hacerlo el método). hacer lo que tiene que hacer).
Las pruebas automatizadas de integración prueban la unidad más grande del código, generalmente toda la aplicación.
Son excelentes para probar casos de uso que no desea probar a mano. Pero también puede tener pruebas de integración manuales, y son tan efectivas (solo que menos convenientes).
Comenzar un nuevo proyecto hoy, no tiene ningún sentido no tener Pruebas Unitarias, pero diría que para un proyecto existente como el tuyo no tiene mucho sentido escribirlas para todo lo que ya tienes y está funcionando.
En su caso, prefiero usar un enfoque de "punto medio" para escribir:
- Pruebas de integración más pequeñas que solo prueban las secciones que va a refactorizar. Si está refactorizando todo el conjunto, entonces puede usar sus Pruebas de integración actuales, pero si está refactorizando solo, digamos, la generación XML, no tiene ningún sentido requerir la presencia de la base de datos, por lo que escribiría un Prueba de integración XML simple y pequeña.
- Un montón de Pruebas Unitarias para el nuevo código que vas a escribir. Como ya escribí anteriormente, las Pruebas Unitarias estarán listas tan pronto como usted "se meta con cualquier cosa en el medio", asegurándose de que su "desastre" vaya a alguna parte.
De hecho, su Prueba de integración solo se asegurará de que su "desorden" no funcione (porque al principio no funcionará, ¿verdad?) Pero no le dará ninguna pista sobre
- por qué no está funcionando
- Si tu depuración del "lío" es realmente arreglar algo
- Si tu depuración del "lío" está rompiendo algo más
Las Pruebas de Integración solo darán la confirmación al final si todo el cambio fue exitoso (y la respuesta será "no" por mucho tiempo). Las Pruebas de Integración no le brindarán ninguna ayuda durante la refactorización, lo que lo hará más difícil y posiblemente frustrante. Necesitas pruebas de unidad para eso.
Estoy de acuerdo con Charlie sobre las pruebas de nivel de integración que corresponden más a las acciones del usuario y la corrección del sistema en su conjunto. Sin embargo, creo que las Pruebas Unitarias tienen mucho más valor que la simple localización de fallas. Las pruebas unitarias proporcionan dos valores principales sobre las pruebas de integración:
1) Escribir pruebas unitarias es tanto un acto de diseño como una prueba. Si practica el desarrollo guiado por pruebas / desarrollo guiado por el comportamiento, el hecho de escribir las pruebas unitarias lo ayuda a diseñar exactamente lo que debe hacer el código. Le ayuda a escribir código de mayor calidad (ya que estar acoplado de forma flexible ayuda con las pruebas) y le ayuda a escribir solo el código suficiente para hacer que se aprueben sus pruebas (ya que sus pruebas están en efecto según su especificación).
2) El segundo valor de las pruebas unitarias es que si se escriben correctamente son muy, muy rápidos. Si realizo un cambio en una clase de su proyecto, ¿puedo realizar todas las pruebas correspondientes para ver si rompí algo? ¿Cómo puedo saber qué pruebas ejecutar? ¿Y cuánto tiempo tomarán? Puedo garantizar que será más largo que las pruebas de unidad bien escritas. Debería poder realizar todas las pruebas unitarias en un par de minutos como máximo.
Joel Spolsky ha escrito un artículo muy interesante sobre pruebas de unidad (fue un diálogo entre Joel y algún otro tipo).
La idea principal era que las pruebas unitarias son muy buenas, pero solo si las usas en cantidades "limitadas". Joel no recomienda alcanzar el estado cuando el 100% de su código se encuentra en casos de prueba.
El problema con las pruebas unitarias es que cuando desea cambiar la arquitectura de su aplicación, tendrá que cambiar todas las pruebas unitarias correspondientes. Y tomará mucho tiempo (tal vez incluso más tiempo que la refactorización en sí). Y después de todo ese trabajo solo fallarán algunas pruebas.
Por lo tanto, escriba pruebas solo para el código que realmente puede causar algunos problemas.
Cómo utilizo las pruebas unitarias: no me gusta el TDD, por lo que primero escribo el código y luego lo pruebo (mediante la consola o el navegador) solo para asegurarme de que este código no funcione correctamente. Y solo después de eso agrego pruebas "difíciles": el 50% de ellas fallan después de las primeras pruebas.
Funciona y no lleva mucho tiempo.
La pregunta tiene una parte filosófica, pero también apunta a consideraciones pragmáticas.
El diseño basado en pruebas utilizado como medio para convertirse en un mejor desarrollador tiene sus méritos, pero no es necesario para eso. Existe un buen programador que nunca escribió una prueba de unidad. La mejor razón para las pruebas unitarias es el poder que le dan al refactorizar, especialmente cuando muchas personas están cambiando la fuente al mismo tiempo. Detectar errores en el registro también es un gran ahorro de tiempo para un proyecto (considere pasar a un modelo de CI y construir el registro en lugar de cada noche). Entonces, si escribe una prueba de unidad, ya sea antes o después de escribir el código que prueba, en ese momento está seguro sobre el nuevo código que ha escrito. Es lo que le puede suceder a ese código más adelante el que la prueba de la unidad se asegure, y eso puede ser significativo. Las pruebas unitarias pueden detener los errores antes de alcanzar el control de calidad, lo que acelera sus proyectos.
Las pruebas de integración enfatizan las interfaces entre los elementos de su pila, si se hacen correctamente. En mi experiencia, la integración es la parte más impredecible de un proyecto. Hacer que las piezas individuales funcionen no suele ser tan difícil, pero juntar todo puede ser muy difícil debido a los tipos de errores que pueden surgir en este paso. En muchos casos, los proyectos se retrasan debido a lo que sucede en la integración. Algunos de los errores encontrados en este paso se encuentran en las interfaces que se han roto por algún cambio realizado en un lado que no se comunicó al otro lado. Otra fuente de errores de integración se encuentra en las configuraciones descubiertas en dev pero que se han olvidado para cuando la aplicación pasa al control de calidad. Las pruebas de integración pueden ayudar a reducir dramáticamente ambos tipos.
La importancia de cada tipo de prueba se puede debatir, pero lo que será más importante para usted es la aplicación de cualquiera de los dos tipos a su situación particular. ¿La aplicación en cuestión está siendo desarrollada por un pequeño grupo de personas o por muchos grupos diferentes? ¿Tiene un repositorio para todo o muchos repositorios para cada componente particular de la aplicación? Si tiene este último, tendrá desafíos con la compatibilidad entre versiones de diferentes componentes.
Cada tipo de prueba está diseñado para exponer los problemas de diferentes niveles de integración en la fase de desarrollo para ahorrar tiempo. Las pruebas unitarias impulsan la integración de la salida de muchos desarrolladores que operan en un repositorio. Las pruebas de integración (mal nombradas) impulsan la integración de componentes en la pila, componentes que a menudo se escriben por equipos separados. La clase de problemas expuestos por las pruebas de integración suelen requerir más tiempo para solucionarlos.
De manera pragmática, realmente se reduce a donde más necesita velocidad en su propio proceso / organización.
Las pruebas unitarias ejecutan métodos en una clase para verificar la entrada / salida adecuada sin probar la clase en el contexto más amplio de su aplicación. Puede usar simulacros para simular clases dependientes; está haciendo pruebas de caja negra de la clase como una entidad independiente. Las pruebas unitarias deben ejecutarse desde una estación de trabajo de desarrollador sin ningún servicio externo o requisitos de software.
Las pruebas de integración incluirán otros componentes de su aplicación y software de terceros (su base de datos de desarrollo Oracle, por ejemplo, o pruebas de Selenium para una aplicación web). Es posible que estas pruebas sigan siendo muy rápidas y se ejecuten como parte de una compilación continua, pero como inyectan dependencias adicionales, también se arriesgan a inyectar nuevos errores que causan problemas en su código, pero que no son causados por su código. Preferiblemente, las pruebas de integración también son donde se inyectan datos reales / grabados y se afirma que la pila de aplicaciones en su conjunto se está comportando como se espera dadas esas entradas.
La pregunta se reduce a qué tipo de errores busca y con qué rapidez espera encontrarlos. Las pruebas unitarias ayudan a reducir la cantidad de errores "simples", mientras que las pruebas de integración lo ayudan a descubrir los problemas de arquitectura e integración, con la esperanza de simular los efectos de la Ley de Murphy en su aplicación en general.
Las pruebas unitarias localizan las fallas más estrechamente. Las pruebas de nivel de integración se corresponden más estrechamente con los requisitos del usuario y, por lo tanto, son mejores predictores del éxito de la entrega. Ninguno de ellos es muy bueno a menos que esté construido y mantenido, pero ambos son muy valiosos si se usan adecuadamente.
(Más...)
Lo que sucede con las pruebas de unidades es que ninguna prueba de nivel de integración puede ejercer todo el código tanto como un buen conjunto de pruebas de unidades. Sí, eso puede significar que tienes que refactorizar las pruebas de alguna manera, pero en general las pruebas no deberían depender tanto de las partes internas. Entonces, digamos, por ejemplo, que tiene una sola función para obtener una potencia de dos. Usted lo describe (como un individuo de métodos formales, yo diría que lo especifica)
long pow2(int p); // returns 2^p for 0 <= p <= 30
Su prueba y su especificación lucen esencialmente iguales (esto es una especie de pseudo-unidad para la ilustración):
assertEqual(1073741824,pow2(30);
assertEqual(1, pow2(0));
assertException(domainError, pow2(-1));
assertException(domainError, pow2(31));
Ahora su implementación puede ser un bucle for con un múltiplo, y puede venir más tarde y cambiar eso a un turno.
Si cambia la implementación para que, digamos, esté devolviendo 16 bits (recuerde que sizeof(long)
solo garantiza que no sea menor que sizeof(short)
), entonces estas pruebas fallarán rápidamente. Una prueba de nivel de integración probablemente debería fallar, pero no ciertamente, y es tan probable que no falle en algún lugar más abajo del cálculo de pow2(28)
.
El punto es que realmente prueban para diferentes situaciones. Si pudiera construir suficientes detalles y pruebas de integración extensas, podría obtener el mismo nivel de cobertura y grado de pruebas de grano fino, pero probablemente sea difícil de hacer, y la explosión exponencial del espacio de estados lo derrotará. Al particionar el espacio de estados usando pruebas unitarias, la cantidad de pruebas que necesita crece mucho menos que de manera exponencial.
Lo que distingue las pruebas unitarias y las pruebas de integración es la cantidad de piezas necesarias para que se ejecute la prueba.
Las pruebas unitarias (teóricamente) requieren muy (o ninguna) otras partes para ejecutarse. Las pruebas de integración (teóricamente) requieren lotes (o todas) otras partes para ejecutarse.
Pruebas de integración prueban el comportamiento Y la infraestructura. Las pruebas unitarias generalmente solo evalúan el comportamiento.
Entonces, las pruebas unitarias son buenas para probar algunas cosas, pruebas de integración para otras cosas.
Entonces, ¿por qué prueba de unidad?
Por ejemplo, es muy difícil probar las condiciones de los límites cuando se realizan pruebas de integración. Ejemplo: una función de back-end espera un entero positivo o 0, el front-end no permite la entrada de un entero negativo, ¿cómo se asegura de que la función de back-end se comporte correctamente cuando le pasa un entero negativo? Tal vez el comportamiento correcto es lanzar una excepción. Esto es muy difícil de hacer con una prueba de integración.
Entonces, para esto, necesitas una prueba unitaria (de la función).
Además, las pruebas unitarias ayudan a eliminar los problemas encontrados durante las pruebas de integración. En su ejemplo anterior, hay muchos puntos de falla para una sola llamada HTTP:
la llamada del cliente HTTP la validación del servlet la llamada del servlet a la capa empresarial la validación de la capa empresarial la base de datos lee (hibernación) la transformación de datos por la capa empresarial la base de datos escribe (hibernación) la transformación de datos -> XML la transformación XSLT -> HTML la transmisión del HTML -> cliente
Para que sus pruebas de integración funcionen, necesita que TODOS estos procesos funcionen correctamente. Para una prueba unitaria de la validación del servlet, solo necesita una. La validación del servlet (que puede ser independiente de todo lo demás). Un problema en una capa se vuelve más fácil de localizar.
Necesita tanto pruebas de unidad como pruebas de integración.
Sólo algunos ejemplos de la experiencia personal:
Pruebas unitarias:
- (+) Mantiene las pruebas cerca del código relevante
- (+) Relativamente fácil de probar todas las rutas de código
- (+) Fácil de ver si alguien cambia inadvertidamente el comportamiento de un método.
- (-) Mucho más difícil de escribir para los componentes de la interfaz de usuario que para los que no son GUI
Pruebas de integración:
- (+) Es bueno tener tuercas y tornillos en un proyecto, pero las pruebas de integración se aseguran de que encajen entre sí
- (-) Más difícil de localizar fuente de errores.
- (-) Más difícil de probar todas las rutas de código (o incluso todas las críticas)
Idealmente ambos son necesarios.
Ejemplos:
Prueba de unidad: asegúrese de que el índice de entrada> = 0 y <longitud de la matriz. ¿Qué sucede cuando fuera de límites? ¿Debería el método lanzar la excepción o devolver el valor nulo?
Prueba de integración: ¿Qué ve el usuario cuando se ingresa un valor de inventario negativo?
El segundo afecta tanto a la interfaz de usuario como al final. Ambos lados podrían funcionar perfectamente, y aún podría obtener la respuesta incorrecta, porque la condición de error entre los dos no está bien definida.
La mejor parte de las pruebas unitarias que hemos encontrado es que hace que los desarrolladores pasen de código-> test-> think to think-> test-> code. Si un desarrollador tiene que escribir la prueba primero, tiende a pensar más en lo que podría salir mal al principio.
Para responder a su última pregunta, ya que las pruebas unitarias viven tan cerca del código y obligan al desarrollador a pensar más al frente, en la práctica hemos encontrado que no tendemos a refactorizar el código tanto, por lo que se mueve menos código - Así que lanzar y escribir nuevas pruebas constantemente no parece ser un problema.
Tenemos 4 tipos diferentes de pruebas en nuestro proyecto:
- Pruebas unitarias con burla cuando sea necesario.
- Pruebas de DB que actúan de manera similar a las pruebas unitarias pero luego tocan db y limpian
- Nuestra lógica se expone a través de REST, por lo que tenemos pruebas que hacen HTTP
- Las pruebas de la aplicación web utilizan WatiN que en realidad usa la instancia de IE y pasa por la funcionalidad principal
Me gustan las pruebas unitarias. Se ejecutan realmente rápido (100-1000x más rápido que las pruebas # 4). Son de tipo seguro, por lo que la refactorización es bastante fácil (con un buen IDE).
El principal problema es cuánto trabajo se requiere para hacerlos correctamente. Tienes que burlarte de todo: acceso a la base de datos, acceso a la red, otros componentes. Tienes que decorar las clases inmóviles, obteniendo un millón de clases en su mayoría inútiles. Tiene que usar DI para que sus componentes no estén bien acoplados y, por lo tanto, no se puedan probar (tenga en cuenta que usar DI no es realmente una desventaja :)
Me gustan las pruebas # 2. Ellos usan la base de datos y reportarán errores en la base de datos, violaciones de restricciones y columnas inválidas. Creo que obtenemos pruebas valiosas usando esto.
# 3 y especialmente # 4 son más problemáticos. Requieren algún subconjunto de entorno de producción en el servidor de compilación. Tienes que construir, implementar y tener la aplicación en ejecución. Tienes que tener una base de datos limpia cada vez. Pero al final, vale la pena. Las pruebas de Watin requieren trabajo constante, pero también se obtienen pruebas constantes. Realizamos pruebas en cada confirmación y es muy fácil de ver cuando rompemos algo.
Entonces, volvamos a tu pregunta. Las pruebas unitarias son rápidas (lo que es muy importante, el tiempo de construcción debe ser inferior a, por ejemplo, 10 minutos) y son fáciles de refactorizar. Mucho más fácil que reescribir todo el tema de watin si su diseño cambia. Si usa un buen editor con un buen comando de búsqueda de usos (por ejemplo, IDEA o VS.NET + Resharper), siempre puede encontrar dónde se está probando su código.
Con las pruebas REST / HTTP, obtiene una buena validación de que su sistema realmente funciona. Pero las pruebas se ejecutan con lentitud, por lo que es difícil tener una validación completa a este nivel. Asumo que sus métodos aceptan múltiples parámetros o posiblemente entrada XML. Para verificar cada nodo en XML o cada parámetro, se necesitarían decenas o cientos de llamadas. Puede hacer eso con pruebas unitarias, pero no puede hacerlo con llamadas REST, cuando cada una puede tomar una gran fracción de segundo.
Nuestras pruebas unitarias verifican las condiciones de los límites especiales con mucha más frecuencia que las pruebas n.º 3. Ellos (# 3) comprueban que la funcionalidad principal está funcionando y eso es todo. Esto parece funcionar bastante bien para nosotros.
Usted podría estar interesado en esta pregunta y las respuestas relacionadas también. Puede encontrar mi adición a las respuestas que ya se dieron aquí (¿es correcto el inglés? :)