¿Cómo mantiene eficientemente sus pruebas trabajando mientras rediseña?

14

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?

Alex Feinman
fuente
¿Cómo es esto significativamente diferente de programmers.stackexchange.com/questions/5898/… ?
AShelly
44
Esa pregunta se hizo por error sobre la refactorización: las pruebas unitarias deberían ser invariables en la refactorización.
Alex Feinman

Respuestas:

9

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.

Cuenta
fuente
Creo que querías escribir "fuera del módulo", no "fuera de la aplicación".
SamB
SamB, depende. Si la interfaz es interna en algunos lugares dentro de una aplicación, pero no pública, consideraría probar en un nivel superior si pensara que es probable que la interfaz sea volátil.
Bill
Este enfoque me pareció muy compatible con TDD. Me gusta comenzar en las capas superiores de la aplicación más cerca del usuario final para poder diseñar las capas inferiores sabiendo cómo las capas superiores necesitan usar las capas inferiores. Esencialmente, construir de arriba hacia abajo le permite diseñar con mayor precisión la interfaz entre una capa y otra.
Greg Burghardt
4

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:

  • Eliminé las clases que iba a reemplazar.
  • Eliminó las suites de prueba para esas clases.
  • Dejó el conjunto de pruebas específico para TIdSipTcpTransport (y todavía existía el conjunto de pruebas común a todos los TIdSipTransports).
  • Ejecuté las pruebas TIdSipTransport / TIdSipTcpTransport, para asegurarme de que todas fallaron.
  • Comentó todas las pruebas TIdSipTransport / TIdSipTcpTransport menos una.
  • Si tuviera que agregar una clase, la agregaría para escribir pruebas para generar suficiente funcionalidad que la única prueba sin comentar pasó.
  • Enjabonar, enjuagar, repetir.

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.

Frank Shearar
fuente
También lo hice hace unos meses y me funcionó bastante bien. Sin embargo, no pude aplicar absolutamente este método al vincularme con un colega en el rediseño desde cero de nuestro módulo de modelo de dominio (que a su vez desencadenó el rediseño de todos los demás módulos en el proyecto).
Marco Ciambrone
3

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:

  • Genere el HTML como prueba de humo para asegurarse de que realmente se ejecute
  • Use un sistema de plantillas, para que pueda probar el procesador de plantillas y los datos enviados a la plantilla, sin probar realmente la plantilla exacta.

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.

Winston Ewert
fuente
También podría ayudar el uso de HTML estructural.
SamB
@SamB ciertamente ayudaría, pero no creo que resuelva el problema por completo
Winston Ewert
por supuesto que no, nada puede :-)
SamB
-1

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).

Lewis Pringle
fuente
Nunca agregue sufijos que no tengan significado o que se autodesprecien de los nombres. Esfuércese siempre por dar nombres significativos.
Basilevs
Te perdiste completamente el punto. Primero, estos no son sufijos que "no tienen significado". Tienen el significado de que la API está pasando de una versión anterior a una más nueva. De hecho, ese es el punto central de la PREGUNTA a la que respondía, y el punto central de la respuesta. Los nombres comunican CLARAMENTE cuál es la API ANTIGUA, cuál es la NUEVA API y cuál es el nombre objetivo de la API una vez que se completa la transición. Y - los sufijos _OLD / _NEW son temporales - SOLO durante la transición de cambio de API.
Lewis Pringle
Buena suerte con la versión NEW_NEW_3 de API tres años después.
Basilevs