Una base de código bien probada tiene varios beneficios, pero probar ciertos aspectos del sistema da como resultado una base de código que es resistente a algunos tipos de cambio.
Un ejemplo es probar resultados específicos, por ejemplo, texto o HTML. Las pruebas a menudo se escriben (ingenuamente) para esperar un bloque de texto en particular como salida para algunos parámetros de entrada, o para buscar secciones específicas en un bloque.
Cambiar el comportamiento del código, para cumplir con nuevos requisitos o porque las pruebas de usabilidad han resultado en un cambio en la interfaz, también requiere cambiar las pruebas, tal vez incluso las pruebas que no son específicamente pruebas unitarias para el código que se está cambiando.
¿Cómo manejas el trabajo de encontrar y reescribir estas pruebas? ¿Qué sucede si no puede simplemente "ejecutarlos todos y dejar que el marco los resuelva"?
¿Qué otro tipo de código bajo prueba resulta en pruebas habitualmente frágiles?
fuente
Respuestas:
Sé que la gente de TDD odiará esta respuesta, pero una gran parte para mí es elegir cuidadosamente dónde probar algo.
Si me vuelvo demasiado loco con las pruebas unitarias en los niveles inferiores, entonces no se pueden hacer cambios significativos sin alterar las pruebas unitarias. Si la interfaz nunca está expuesta y no está destinada a ser reutilizada fuera de la aplicación, esto es una sobrecarga innecesaria de lo que podría haber sido un cambio rápido de lo contrario.
Por el contrario, si lo que está tratando de cambiar está expuesto o reutilizado, cada una de esas pruebas que tendrá que cambiar es evidencia de algo que podría estar rompiendo en otro lugar.
En algunos proyectos, esto puede equivaler a diseñar sus pruebas desde el nivel de aceptación hacia abajo en lugar de desde las pruebas unitarias hacia arriba. y tener menos pruebas unitarias y más pruebas de estilo de integración.
No significa que aún no pueda identificar una sola característica y código hasta que esa característica cumpla con sus criterios de aceptación. Simplemente significa que en algunos casos no terminas midiendo los criterios de aceptación con pruebas unitarias.
fuente
Acabo de completar una revisión importante de mi pila SIP, reescribiendo todo el transporte TCP. (Esta fue una refactorización cercana, en gran escala, en relación con la mayoría de las refactorizaciones).
En resumen, hay un TIdSipTcpTransport, subclase de TIdSipTransport. Todos los TIdSipTransports comparten un conjunto de pruebas común. Dentro de TIdSipTcpTransport había una serie de clases: un mapa que contenía pares de conexión / mensaje de inicio, clientes TCP roscados, un servidor TCP roscado, etc.
Esto es lo que hice:
Por lo tanto, sabía lo que aún tenía que hacer, en forma de pruebas comentadas (*), y sabía que el nuevo código funcionaba como se esperaba, gracias a las nuevas pruebas que escribí.
(*) Realmente, no necesitas comentarlos. Simplemente no los ejecutes; 100 pruebas fallidas no son muy alentadoras. Además, en mi configuración particular, compilar menos pruebas significa un ciclo de prueba-escritura-refactorización más rápido.
fuente
Cuando las pruebas son frágiles, encuentro que generalmente es porque estoy probando algo incorrecto. Tomemos, por ejemplo, la salida HTML. Si verifica la salida HTML real, su prueba será frágil. Pero no le interesa el resultado real, le interesa saber si transmite la información que debería. Desafortunadamente, hacer eso requiere hacer afirmaciones sobre el contenido del cerebro del usuario y, por lo tanto, no se puede hacer automáticamente.
Usted puede:
Lo mismo sucede con SQL. Si afirma el SQL real que sus clases intentan hacer, tendrá problemas. Realmente quieres afirmar los resultados. Por lo tanto, uso una base de datos de memoria SQLITE durante mis pruebas unitarias para asegurarme de que mi SQL realmente haga lo que se supone que debe hacer.
fuente
Primero cree una NUEVA API, que haga lo que quiere que sea su NUEVO comportamiento de API. Si sucede que esta nueva API tiene el mismo nombre que una API ANTERIOR, entonces agrego el nombre _NUEVO al nuevo nombre de la API.
int DoSomethingInterestingAPI ();
se convierte en:
int DoSomethingInterestingAPI_NEW (int takes_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API); OK, en esta etapa, todas sus pruebas de regresión todavía pasan, usando el nombre DoSomethingInterestingAPI ().
SIGUIENTE, revise su código y cambie todas las llamadas a DoSomethingInterestingAPI () a la variante apropiada de DoSomethingInterestingAPI_NEW (). Esto incluye actualizar / reescribir las partes de las pruebas de regresión que se deban cambiar para usar la nueva API.
SIGUIENTE, marque DoSomethingInterestingAPI_OLD () como [[en desuso ()]]. Mantenga la API obsoleta todo el tiempo que desee (hasta que haya actualizado de forma segura todo el código que pueda depender de ella).
Con este enfoque, cualquier falla en sus pruebas de regresión simplemente son errores en esa prueba de regresión o identifican errores en su código, exactamente como lo desearía. Este proceso por etapas de revisión de una API mediante la creación explícita de versiones _NEW y _OLD de la API le permite tener partes del código nuevo y antiguo coexistiendo por un tiempo.
Aquí hay un buen (difícil) ejemplo de este enfoque en la práctica. Tenía la función BitSubstring (), donde había utilizado el enfoque de tener el tercer parámetro como el CONTEO de bits en la subcadena. Para ser coherente con otras API y patrones en C ++, quería cambiar para comenzar / finalizar como argumentos de la función.
https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0
Creé una función BitSubstring_NEW con la nueva API y actualicé todo mi código para usarla (sin dejar MÁS LLAMADAS a BitSubString). Pero me dejé en la implementación durante varios lanzamientos (meses), y lo marqué como obsoleto, para que todos pudieran cambiar a BitSubString_NEW (y en ese momento cambiar el argumento de un conteo al estilo de inicio / finalización).
ENTONCES: cuando se completó esa transición, hice otra confirmación eliminando BitSubString () y renombrando BitSubString_NEW-> BitSubString () (y desaprobé el nombre BitSubString_NEW).
fuente