Breve introducción a esta pregunta. He usado TDD ahora y últimamente BDD por más de un año. Utilizo técnicas como burlarse para que escribir mis exámenes sea más eficiente. Últimamente he comenzado un proyecto personal para escribir un pequeño programa de administración de dinero para mí. Como no tenía código heredado, fue el proyecto perfecto para comenzar con TDD. Desafortunadamente, no experimenté tanto la alegría de TDD. Incluso estropeó mi diversión tanto que he renunciado al proyecto.
¿Cual fue el problema? Bueno, he usado el enfoque similar a TDD para permitir que las pruebas / requisitos evolucionen el diseño del programa. El problema era que más de la mitad del tiempo de desarrollo de las pruebas de escritura / refactorización. Así que al final no quería implementar más funciones porque necesitaría refactorizar y escribir en muchas pruebas.
En el trabajo tengo mucho código heredado. Aquí escribo más y más pruebas de integración y aceptación y menos pruebas unitarias. Esto no parece ser un mal enfoque ya que los errores son detectados principalmente por las pruebas de aceptación e integración.
Mi idea era que al final podría escribir más pruebas de integración y aceptación que pruebas unitarias. Como dije para detectar errores, las pruebas unitarias no son mejores que las pruebas de integración / aceptación. Las pruebas unitarias también son buenas para el diseño. Como solía escribir muchos de ellos, mis clases siempre están diseñadas para ser comprobables. Además, el enfoque para permitir que las pruebas / requisitos guíen el diseño conduce en la mayoría de los casos a un mejor diseño. La última ventaja de las pruebas unitarias es que son más rápidas. He escrito suficientes pruebas de integración para saber que pueden ser casi tan rápidas como las pruebas unitarias.
Después de buscar en la web, descubrí que hay ideas muy similares a las mías mencionadas aquí y allá . ¿Qué piensas de esta idea?
Editar
Respondiendo a las preguntas, un ejemplo en el que el diseño era bueno, pero necesitaba una gran refactorización para el siguiente requisito:
Al principio había algunos requisitos para ejecutar ciertos comandos. Escribí un analizador de comandos extensible, que analizaba los comandos de algún tipo de símbolo del sistema y llamaba al correcto en el modelo. El resultado se representó en una clase de modelo de vista:
No había nada malo aquí. Todas las clases eran independientes entre sí y podía agregar fácilmente nuevos comandos, mostrar nuevos datos.
El siguiente requisito era que cada comando debería tener su propia representación de vista, algún tipo de vista previa del resultado del comando. Rediseñé el programa para lograr un mejor diseño para el nuevo requisito:
Esto también fue bueno porque ahora cada comando tiene su propio modelo de vista y, por lo tanto, su propia vista previa.
La cuestión es que el analizador de comandos se cambió para usar un análisis basado en tokens de los comandos y se despojó de su capacidad para ejecutar los comandos. Cada comando tiene su propio modelo de vista y el modelo de vista de datos solo conoce el modelo de vista de comando actual que conoce los datos que se deben mostrar.
Todo lo que quería saber en este momento es si el nuevo diseño no rompió ningún requisito existente. No tuve que cambiar CUALQUIERA de mi prueba de aceptación. Tuve que refactorizar o eliminar casi CADA prueba unitaria, lo que fue una gran cantidad de trabajo.
Lo que quería mostrar aquí es una situación común que sucedió a menudo durante el desarrollo. No hubo ningún problema con los diseños antiguos o nuevos, simplemente cambiaron naturalmente con los requisitos: cómo lo entendí, esta es una ventaja de TDD, que el diseño evoluciona.
Conclusión
Gracias por todas las respuestas y discusiones. En resumen de esta discusión, he pensado en un enfoque que probaré con mi próximo proyecto.
- En primer lugar, escribo todas las pruebas antes de implementar algo como siempre lo hice.
- Para requisitos, primero escribo algunas pruebas de aceptación que prueban todo el programa. Luego escribo algunas pruebas de integración para los componentes donde necesito implementar el requisito. Si hay un componente que trabaje estrechamente con otro componente para implementar este requisito, también escribiría algunas pruebas de integración donde ambos componentes se prueban juntos. Por último, pero no menos importante, si tengo que escribir un algoritmo o cualquier otra clase con una permutación alta, por ejemplo, un serializador, escribiría pruebas unitarias para estas clases en particular. Todas las otras clases no se prueban, pero ninguna prueba unitaria.
- Para errores, el proceso puede simplificarse. Normalmente un error es causado por uno o dos componentes. En este caso, escribiría una prueba de integración para los componentes que prueban el error. Si se relacionara con un algoritmo, solo escribiría una prueba unitaria. Si no es fácil detectar el componente donde se produce el error, escribiría una prueba de aceptación para localizar el error; esto debería ser una excepción.
fuente
Respuestas:
Está comparando naranjas y manzanas.
Pruebas de integración, pruebas de aceptación, pruebas de unidad, pruebas de comportamiento: todas son pruebas y todas te ayudarán a mejorar tu código, pero también son bastante diferentes.
Voy a repasar cada una de las diferentes pruebas en mi opinión y espero explicar por qué necesita una combinación de todas ellas:
Pruebas de integración:
Simplemente, compruebe que las diferentes partes componentes de su sistema se integran correctamente, por ejemplo, tal vez simule una solicitud de servicio web y verifique que el resultado regrese. En general, usaría datos estáticos reales (ish) y dependencias simuladas para garantizar que se puedan verificar de manera consistente.
Prueba de aceptacion:
Una prueba de aceptación debe correlacionarse directamente con un caso de uso comercial. Puede ser enorme ("las transacciones se envían correctamente") o pequeña ("el filtro filtra una lista con éxito"), no importa; lo que importa es que debe estar explícitamente vinculado a un requisito específico del usuario. Me gusta centrarme en estos para el desarrollo basado en pruebas porque significa que tenemos un buen manual de referencia de pruebas para historias de usuarios para que dev y qa verifiquen.
Pruebas unitarias:
Para pequeñas unidades discretas de funcionalidad que pueden o no componer una historia de usuario individual por sí misma, por ejemplo, una historia de usuario que dice que recuperamos a todos los clientes cuando accedemos a una página web específica puede ser una prueba de aceptación (simular golpear la web página y verificar la respuesta), pero también puede contener varias pruebas unitarias (verifique que los permisos de seguridad estén verificados, verifique que la conexión de la base de datos consulte correctamente, verifique que cualquier código que limite el número de resultados se ejecute correctamente): todas estas son "pruebas unitarias" eso no es una prueba de aceptación completa.
Pruebas de comportamiento:
Defina cuál debería ser el flujo de una aplicación en el caso de una entrada específica. Por ejemplo, "cuando no se puede establecer la conexión, verifique que el sistema vuelva a intentar la conexión". Nuevamente, es poco probable que sea una prueba de aceptación completa, pero aún así le permite verificar algo útil.
Todo esto está en mi opinión a través de mucha experiencia en pruebas de escritura; No me gusta centrarme en los enfoques de los libros de texto, sino centrarme en lo que le da valor a tus pruebas.
fuente
TL; DR: siempre y cuando satisfaga sus necesidades, sí.
Llevo muchos años haciendo desarrollo de desarrollo impulsado por pruebas de aceptación (ATDD). Puede ser muy exitoso. Hay algunas cosas a tener en cuenta.
Ahora los beneficios
Como siempre, depende de usted hacer el análisis y determinar si esta práctica es adecuada para su situación. A diferencia de muchas personas, no creo que haya una respuesta correcta idealizada. Dependerá de sus necesidades y requisitos.
fuente
Las pruebas unitarias funcionan mejor cuando la interfaz pública de los componentes para los que se utilizan no cambia con demasiada frecuencia. Esto significa, cuando los componentes ya están bien diseñados (por ejemplo, siguiendo los principios SÓLIDOS).
Por lo tanto, creer que un buen diseño simplemente "evoluciona" de "lanzar" muchas pruebas unitarias a un componente es una falacia. TDD no es "maestro" para un buen diseño, solo puede ayudar un poco a verificar que ciertos aspectos del diseño sean buenos (especialmente la capacidad de prueba).
Cuando sus requisitos cambian, y tiene que cambiar los componentes internos de un componente, y esto romperá el 90% de las pruebas de su unidad, por lo que debe refactorizarlas muy a menudo, entonces el diseño probablemente no fue tan bueno.
Entonces, mi consejo es: piense en el diseño de los componentes que ha creado y en cómo puede hacerlos más siguiendo el principio abierto / cerrado. La idea de este último es asegurarse de que la funcionalidad de sus componentes se pueda extender más tarde sin cambiarlos (y así no romper la API del componente utilizada por sus pruebas unitarias). Dichos componentes pueden (y deben) cubrirse mediante pruebas de prueba unitarias, y la experiencia no debe ser tan dolorosa como la ha descrito.
Cuando no se puede llegar a tal diseño de inmediato, las pruebas de aceptación e integración pueden ser un mejor comienzo.
EDITAR: a veces el diseño de sus componentes puede estar bien, pero el diseño de las pruebas de su unidad puede causar problemas . Ejemplo simple: desea probar el método "MyMethod" de la clase X y escribir
(suponga que los valores tienen algún tipo de significado).
Supongamos además que en el código de producción solo hay una llamada
X.MyMethod
. Ahora, para un nuevo requisito, el método "MyMethod" necesita un parámetro adicional (por ejemplo, algo así comocontext
), que no se puede omitir. Sin pruebas unitarias, uno tendría que refactorizar el código de llamada en un solo lugar. Con las pruebas unitarias, uno tiene que refactorizar 500 lugares.Pero la causa aquí no es que la unidad se pruebe a sí misma, es solo el hecho de que la misma llamada a "X.MyMethod" se repite una y otra vez, sin seguir estrictamente el principio "No te repitas (DRY). Así que la solución aquí es poner los datos de prueba y los valores esperados relacionados en una lista y ejecutar las llamadas a "MyMethod" en un bucle (o, si la herramienta de prueba admite las llamadas "pruebas de manejo de datos", para usar esa función). el número de lugares para cambiar en las pruebas unitarias cuando la firma del método cambia a 1 (en lugar de 500).
En su caso del mundo real, la situación puede ser más compleja, pero espero que se haga una idea: cuando las pruebas de su unidad usen una API de componentes para la que no sabe si puede estar sujeta a cambios, asegúrese de reducir el número de llamadas a esa API al mínimo.
fuente
X x= new X(); AssertTrue(x.MyMethod(12,"abc"))
antes de implementar el método. Usando un diseño inicial, puede escribirclass X{ public bool MyMethod(int p, string q){/*...*/}}
primero y escribir las pruebas más tarde. En ambos casos, ha tomado la misma decisión de diseño. Si la decisión fue buena o mala, TDD no se lo informará.Sí, claro que lo es.
Considera esto:
Ver la diferencia general ...
El problema es uno de cobertura de código, si puede lograr una prueba completa de todo su código usando pruebas de integración / aceptación, entonces no hay problema. Tu código ha sido probado. Ese es el objetivo.
Creo que es posible que deba mezclarlos, ya que cada proyecto basado en TDD requerirá algunas pruebas de integración solo para asegurarse de que todas las unidades realmente funcionen bien juntas (sé por experiencia que una base de código probada al 100% no necesariamente funciona cuando los pones todos juntos!)
El problema realmente se reduce a la facilidad de probar, depurar las fallas y corregirlas. Algunas personas encuentran que sus pruebas unitarias son muy buenas para esto, son pequeñas y simples y las fallas son fáciles de ver, pero la desventaja es que tienes que reorganizar tu código para adaptarlo a las herramientas de prueba unitaria, y escribir muchas de ellas. Una prueba de integración es más difícil de escribir para cubrir una gran cantidad de código, y probablemente tendrá que usar técnicas como iniciar sesión para depurar cualquier falla (aunque, diría que tiene que hacer esto de todos modos, no puede fallar la prueba unitaria cuando en el sitio!).
Sin embargo, de cualquier manera, aún obtienes código probado, solo tienes que decidir qué mecanismo te conviene más. (Me gustaría mezclar un poco, probar unitariamente los algoritmos complejos e integrar la prueba del resto).
fuente
Creo que es una idea horrible.
Dado que las pruebas de aceptación y la prueba de integración tocan partes más amplias de su código para probar un objetivo específico, necesitarán más refactorización con el tiempo, no menos. Peor aún, dado que cubren secciones amplias del código, aumentan el tiempo que pasa rastreando la causa raíz ya que tiene un área más amplia para buscar.
No, por lo general, debe escribir más pruebas unitarias a menos que tenga una aplicación impar que tenga una interfaz de usuario del 90% o algo más que sea incómodo para la prueba unitaria. El dolor con el que te encuentras no proviene de las pruebas unitarias, sino de hacer el primer desarrollo de la prueba. En general, solo debe pasar 1/3 de su tiempo en la mayoría de las pruebas de escritura. Después de todo, están ahí para servirle, no al revés.
fuente
El "triunfo" con TDD es que una vez que las pruebas se han escrito, pueden automatizarse. La otra cara es que puede consumir una parte significativa del tiempo de desarrollo. Si esto realmente ralentiza todo el proceso es discutible. El argumento es que las pruebas iniciales reducen el número de errores que se corregirán al final del ciclo de desarrollo.
Aquí es donde entra BDD, ya que los comportamientos se pueden incluir dentro de las pruebas unitarias, por lo que el proceso es, por definición, menos abstracto y más tangible.
Claramente, si hubiera disponible una cantidad de tiempo infinita, haría tantas pruebas de diversas variedades como fuera posible. Sin embargo, el tiempo es generalmente limitado y las pruebas continuas solo son rentables hasta cierto punto.
Todo esto lleva a la conclusión de que las pruebas que proporcionan el mayor valor deben estar al frente del proceso. Esto en sí mismo no favorece automáticamente un tipo de prueba sobre otro, más que cada caso debe tomarse según sus méritos.
Si está escribiendo un widget de línea de comando para uso personal, le interesarían principalmente las pruebas unitarias. Mientras que un servicio web, por ejemplo, requeriría una cantidad sustancial de pruebas de integración / comportamiento.
Si bien la mayoría de los tipos de pruebas se concentran en lo que podría llamarse la "línea de carreras", es decir, probar lo que requiere el negocio hoy en día, las pruebas unitarias son excelentes para eliminar errores sutiles que podrían surgir en las fases de desarrollo posteriores. Dado que este es un beneficio que no se puede medir fácilmente, a menudo se pasa por alto.
fuente
Este es el punto clave, y no solo "la última ventaja". Cuando el proyecto se hace más y más grande, sus pruebas de aceptación de integración son cada vez más lentas. Y aquí, quiero decir tan lento que vas a dejar de ejecutarlos.
Por supuesto, las pruebas unitarias también se están volviendo más lentas, pero aún son más rápidas que el orden de magnitud. Por ejemplo, en mi proyecto anterior (c ++, unos 600 kLOC, 4000 pruebas unitarias y 200 pruebas de integración), me llevó aproximadamente un minuto ejecutar todas y más de 15 para ejecutar las pruebas de integración. Para construir y ejecutar pruebas unitarias para la parte que se está cambiando, tomaría menos de 30 segundos en promedio. Cuando puedas hacerlo tan rápido, querrás hacerlo todo el tiempo.
Solo para dejarlo claro: no digo que no agregue pruebas de integración y aceptación, pero parece que lo hizo TDD / BDD de manera incorrecta.
Sí, diseñar teniendo en cuenta la capacidad de prueba mejorará el diseño.
Bueno, cuando los requisitos cambian, tienes que cambiar el código. Te diría que no terminaste tu trabajo si no escribiste pruebas unitarias. Pero esto no significa que deba tener una cobertura del 100% con las pruebas unitarias; ese no es el objetivo. Algunas cosas (como GUI, o acceder a un archivo, ...) ni siquiera están destinadas a ser probadas por la unidad.
El resultado de esto es una mejor calidad de código y otra capa de prueba. Yo diría que vale la pena.
También tuvimos varias pruebas de aceptación de miles, y tomaría toda una semana ejecutarlas todas.
fuente