¿Cómo hace que sus pruebas funcionen de manera eficiente al rediseñar?

14

Una base de código bien probada tiene varios beneficios, pero al probar ciertos aspectos del sistema se obtiene una base de código que es resistente a algunos tipos de cambio.

Un ejemplo es probar una salida específica, por ejemplo, texto o HTML. Las pruebas a menudo se escriben (¿ingenuamente?) Para esperar un bloque particular de texto 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 los nuevos requisitos o porque las pruebas de usabilidad han dado lugar a cambios 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 gestionas 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?

pregunta Alex Feinman 21.09.2010 - 16:37

4 respuestas

8

Sé que la gente de TDD odiará esta respuesta, pero para mí una gran parte es elegir cuidadosamente dónde probar algo.

Si me vuelvo loco con las pruebas unitarias en los niveles inferiores, no se pueden realizar cambios significativos sin alterar las pruebas unitarias. Si la interfaz nunca está expuesta y no está diseñada para ser reutilizada fuera de la aplicación, esto es simplemente una sobrecarga innecesaria de lo que podría haber sido un cambio rápido de lo contrario.

A la inversa, si lo que está tratando de cambiar se expone o se reutiliza, cada una de las pruebas que tendrá que cambiar es evidencia de algo que podría estar compartiendo en otra parte.

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 de unidad hasta. y tener menos pruebas unitarias y más pruebas de estilo de integración.

No significa que aún no pueda identificar una sola función 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 las pruebas unitarias.

    
respondido por el Bill 21.09.2010 - 18:17
4

Acabo de completar una revisión importante de mi pila SIP, reescribiendo todo el transporte TCP. (Esto fue casi un refactor, en una escala bastante grande, 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 se encontraban varias clases: un mapa que contiene pares de conexión / mensaje de inicio, clientes TCP con hilos, un servidor TCP con hilos, etc.

Esto es lo que hice:

  • Se eliminaron las clases que iba a reemplazar.
  • Se eliminaron las suites de prueba para esas clases.
  • Izquierda el conjunto de pruebas específico para TIdSipTcpTransport (y todavía existía el conjunto de pruebas común para todos los TIdSipTransports).
  • Corrió las pruebas TIdSipTransport / TIdSipTcpTransport, para asegurarse de que todas fallaron.
  • Comentó todas las pruebas TIdSipTransport / TIdSipTcpTransport excepto una.
  • Si tuviera que agregar una clase, la agregaría a las pruebas de escritura para desarrollar la funcionalidad suficiente que solo pasó la prueba sin comentarios.
  • Hacer espuma, enjuagar, repetir.

Por lo tanto, sabía lo que todavía 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 es necesario comentarlos. Simplemente no los ejecute; 100 pruebas fallidas no son muy alentadoras. Además, en mi configuración particular, compilar menos pruebas significa un bucle de prueba-escritura-refactor más rápido.

    
respondido por el Frank Shearar 21.09.2010 - 18:39
3

Cuando las pruebas son frágiles, generalmente lo encuentro porque estoy probando algo incorrecto. Tomemos, por ejemplo, la salida HTML. Si verifica el resultado HTML real, su prueba será frágil. Pero no está interesado en el resultado real, está interesado en si transmite la información que debería. Desafortunadamente, hacer eso requiere hacer afirmaciones sobre el contenido de los cerebros del usuario y, por lo tanto, no se puede hacer automáticamente.

Puedes:

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

El mismo tipo de cosas sucede con SQL. Si afirmas el SQL real, tus clases intentarán hacer que vayas a tener problemas. Usted realmente quiere afirmar los resultados. Por lo tanto, uso una base de datos de memoria SQLITE durante mis pruebas de unidad para asegurarme de que mi SQL realmente haga lo que se supone que debe hacer.

    
respondido por el Winston Ewert 21.09.2010 - 19:19
-1

Primero cree una API NUEVA, que haga lo que usted quiere que sea su comportamiento de API NUEVA. Si sucede que esta nueva API tiene el mismo nombre que una API ANTIGUA, entonces agrego el nombre _NEW 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); Bien, en esta etapa, todas sus pruebas de regresión se pasarán, usando el nombre DoSomethingInterestingAPI ().

SIGUIENTE, revisa tu código y cambia todas las llamadas a DoSomethingInterestingAPI () a la variante apropiada de DoSomethingInterestingAPI_NEW (). Esto incluye actualizar / reescribir cualquier parte de sus pruebas de regresión que deba cambiarse para usar la nueva API.

SIGUIENTE, marca DoSomethingInterestingAPI_OLD () como [[obsoleto ()]]. Mantente cerca de la API obsoleta todo el tiempo que quieras (hasta que hayas actualizado de forma segura todo el código que pueda depender de él).

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 desearía. Este proceso por etapas de revisión de una API mediante la creación explícita de las versiones _NEW y _OLD de la API le permite tener bits del código nuevo y antiguo coexistiendo por un tiempo.

Este es un buen ejemplo (difícil) de este enfoque en la práctica. Tenía la función BitSubstring (), en la que había utilizado el método de tener el tercer parámetro como COUNT de bits en la subcadena. Para ser coherente con otras API y patrones en C ++, quise cambiar para comenzar / terminar como argumentos a la función.

enlace

Creé una función BitSubstring_NEW con la nueva API y actualicé todo mi código para usarlo (dejando NO MÁS LLAMADAS a BitSubString). Pero dejé la implementación para varias versiones (meses), y la marqué en desuso, para que todos puedan cambiar a BitSubString_NEW (y en ese momento, cambiar el argumento de una cuenta para comenzar / finalizar el estilo).

ENTONCES: cuando se completó la transición, hice otro compromiso eliminando BitSubString () y cambiando el nombre de BitSubString_NEW- > BitSubString () (y desaprobé el nombre de BitSubString_NEW).

    
respondido por el Lewis Pringle 17.07.2018 - 04:02

Lea otras preguntas en las etiquetas