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.