2009-09-15 12 views
5

En el artículo Test for Required Behavior, not Incidental Behavior, Kevlin Henney nos informa que:Pruebas de comportamiento requerido vs TDD

"[...] un error común en las pruebas es cablear las pruebas a las características específicas de una aplicación, donde esos detalles son incidentales y no tienen relación con la funcionalidad deseada ".

Sin embargo, cuando uso TDD, a menudo termino escribiendo pruebas de comportamiento incidental. ¿Qué hago con estas pruebas? Tirarlas parece estar mal, pero el consejo en el artículo es que estas pruebas pueden reducir la agilidad.

¿Qué hay de separarlos en un conjunto de pruebas por separado? Eso suena como un comienzo, pero parece impráctico intuitivamente. ¿Alguien hace esto?

+0

Creo que ayudaría si incluyera un ejemplo. Definitivamente una pregunta válida, pero podría interpretarse de diferentes maneras. –

Respuesta

3

En mi experiencia, las pruebas dependientes de la implementación son frágiles y fallarán enormemente en la primera refactorización. Lo que trato de hacer es centrarme en obtener una interfaz adecuada para una clase mientras escribo las pruebas, evitando efectivamente los detalles de implementación en la interfaz. Esto no solo resuelve las pruebas frágiles, sino que también promueve un diseño más limpio.

Esto todavía permite pruebas adicionales que verifican las partes riesgosas de mi implementación seleccionada, pero solo como una protección adicional a una buena cobertura de la interfaz "normal" de mi clase.

Para mí, el gran cambio de paradigma se produjo cuando empecé a escribir pruebas antes de siquiera pensar en la implementación. Mi sorpresa inicial fue que se hizo mucho más fácil generar casos de prueba "extremos". Luego, reconocí que la interfaz mejorada a su vez ayudó a dar forma a la implementación detrás de ella. El resultado es que mi código hoy en día no hace mucho más de lo que expone la interfaz, reduciendo efectivamente la necesidad de la mayoría de las pruebas de "implementación".

Durante la refabricación de las partes internas de una clase, todas las pruebas se mantendrán. Solo en los casos en que la interfaz expuesta cambia, es posible que se deba extender o modificar el conjunto de prueba.

+0

Hola Timo. Gracias por tu respuesta. La dificultad radica en que, con TDD, las pruebas impulsan la implementación y, por lo tanto, dependen por definición de la implementación. Está bien, hasta donde sé, mi implementación está cubierta al 100% por la prueba, y puedo refactorizar libremente la implementación (usando la definición de refactorización de Feathers, es decir, el comportamiento no cambia). El problema que tengo es con la filosofía de "no evaluar el comportamiento incidental": estéticamente tiene sentido para mí, pero prácticamente no veo cómo puedo hacerlo * sin * perder la cobertura de implementación que brindan mis pruebas TDD. –

+0

(ahora agregué mis opiniones personales de primera prueba a mi respuesta.) – Timo

1

El problema que describes es muy real y muy fácil de encontrar cuando TDD'ing. En general, puede decir que no está probando el comportamiento incidental en sí mismo, que es un problema, sino más bien si toneladas de pruebas dependen de ese comportamiento incidental.

El principio DRY se aplica tanto al código de prueba como al código de producción. Eso a menudo puede ser una buena guía al escribir el código de prueba. El objetivo debe ser que todo el comportamiento 'incidental' que especifique en el camino esté aislado, de manera que solo unas pocas pruebas de todo el conjunto de pruebas lo utilicen. De esta forma, si necesita refaccionar ese comportamiento, solo necesita modificar algunas pruebas en lugar de una gran parte de todo el conjunto de pruebas.

Esto se logra mejor mediante el uso copioso de interfaces o clases abstractas como colaboradores, porque esto significa que obtiene un bajo nivel de acoplamiento.

Aquí hay un ejemplo de lo que quiero decir. Supongamos que tiene algún tipo de implementación de MVC donde un Controlador debe devolver una Vista. Supongamos que tenemos un método de este tipo en un BookController:

public View DisplayBookDetails(int bookId) 

La aplicación debe utilizar un IBookRepository inyectada para conseguir el libro de la base de datos y luego convertir que a una vista de ese libro. Podría escribir muchas pruebas para cubrir todos los aspectos del método DisplayBookDetails, pero también podría hacer otra cosa:

Defina una interfaz IBookMapper adicional e inyéctela en el BookController además del IBookRepository. La aplicación del método podría entonces ser algo como esto:

public View DisplayBookDetails(int bookId) 
{ 
    return this.mapper.Map(this.repository.GetBook(bookId); 
} 

Obviamente, esto es un ejemplo demasiado simplista, pero el punto es que ahora se puede escribir una serie de pruebas para su aplicación IBookMapper real, lo que significa que cuando Si prueba el método DisplayBookDetails, puede usar un Stub (mejor generado por un marco simulado dinámico) para implementar la asignación, en lugar de tratar de definir una relación frágil y compleja entre un objeto del Dominio del Libro y cómo se mapea.

El uso de un IBookMaper es definitivamente un detalle de implementación incidental, pero si usa un contenedor SUT Factory o mejor aún un contenedor de burla automática, la definición de ese comportamiento incidental está aislada, lo que significa que si más adelante decide refactorizar la implementación, puede hacerlo cambiando solo el código de prueba en algunos lugares.

1

"¿Qué hay de separarlos en un conjunto de pruebas por separado?"

¿Qué harías con esa suite separada?

Aquí está el caso de uso típico.

  1. Ha escrito algunas pruebas que prueban los detalles de implementación que no deberían haber probado.

  2. Factoriza esas pruebas fuera de la suite principal en una suite aparte.

  3. Alguien cambia la implementación.

  4. Su conjunto de aplicaciones ahora falla (como debería).

¿Ahora qué?

  • ¿Reparan las pruebas de implementación? Yo creo que no. El punto era no probar una implementación porque lleva a mucho trabajo de mantenimiento.

  • ¿Las pruebas pueden fallar, pero la ejecución total de la unidad todavía se considera buena? Si las pruebas fallan, pero la falla no importa, ¿qué significa eso? [Lea esta pregunta para un ejemplo: Non-critical unittest failures Una prueba ignorada o irrelevante es simplemente costosa.

Tienes que descartarlos.

Ahórrese un poco de tiempo y empeño desechándolos ahora, no cuando fallen.

0

Te realmente hacer TDD el problema no es tan grande como puede parecer a la vez porque está escribiendo pruebas antes código. Ni siquiera debería pensar en una posible implementación antes de escribir la prueba.

Tales problemas de prueba de comportamiento incidental son mucho más comunes cuando se escriben pruebas después del código de implementación. Entonces, la manera más fácil es simplemente verificar que la salida de la función sea correcta y hacer lo que quiera, luego escribir la prueba usando esa salida. Realmente eso es hacer trampa, no TDD, y el costo de hacer trampa es pruebas que se romperán si la implementación cambia.

Lo bueno es que tales pruebas se romperán aún más fácilmente que buenas pruebas (buena prueba lo que significa que aquí las pruebas dependen solo de la función deseada, no dependen de la implementación). Tener exámenes tan genéricos que nunca se rompen es bastante peor.

Donde trabajo lo que hacemos es simplemente corregir tales pruebas cuando nos topamos con ellas. Cómo los solucionamos depende del tipo de prueba incidental realizada.

  • la prueba de este tipo más común es probablemente el caso en el que se produce resultados de las pruebas en un orden definido con vistas a este fin en realidad no está garantizada. La solución fácil es bastante simple: clasifique el resultado y el resultado esperado. Para estructuras más complejas, use algún comparador que ignore ese tipo de diferencias.

  • cada cierto tiempo probamos la función más interna, mientras que es una función más externa la que realiza la función. Eso es malo porque la refacturación de la función más interna se vuelve difícil. La solución es escribir otra prueba que cubra el mismo rango de funciones en el nivel de función más externo, luego eliminar la prueba anterior, y solo entonces podemos refactorizar el código.

  • cuando tal salto de prueba y vemos una manera fácil de hacer que la implementación sea independiente lo hacemos. Sin embargo, si no es fácil, podemos elegir repararlos para que sigan dependiendo de la implementación, pero dependiendo de la nueva implementación. Las pruebas volverán a fallar en el próximo cambio de implementación, pero no es necesariamente un gran problema. Si es un gran problema, entonces descarta definitivamente esa prueba y encuentra otra para cubrir esa característica, o cambia el código para que sea más fácil de evaluar.

  • Otro caso malo es cuando tenemos pruebas escritas utilizando algún objeto burlado (utilizado como stub) y luego cambia el comportamiento del objeto burlado (cambio de API). Éste es malo porque no rompe el código cuando debería porque cambiar el comportamiento del objeto burlado no cambiará el simulacro que lo imita. La solución aquí es usar el objeto real en lugar del simulacro si es posible, o arreglar el simulacro para un nuevo comportamiento. En ese caso, tanto el comportamiento falso como el comportamiento real del objeto son incidentales, pero creemos que las pruebas que no fallan cuando deberían son un problema mayor que las pruebas que se rompen cuando no deberían. (Admitidamente tales casos también pueden ser atendidos en el nivel de pruebas de integración).

+0

Hola kriss. Gracias por tu respuesta. Estamos haciendo TDD en el verdadero sentido, y la implementación está completamente impulsada por pruebas (siguiendo las reglas minimalistas de Uncle Bob, escribiendo lo suficiente de una prueba, etc.), pero eso significa que las pruebas * por definición * son específicas de la implementación, correcta ? Creo que podría estar bien en algún sentido, siempre y cuando las pruebas prueben componentes lo suficientemente pequeños, y los componentes sean separables. Pero, ¿cómo organizar las pruebas de comportamiento, vs implementación? ¿Y existe una superposición entre las pruebas incidentales (malas) y las pruebas de implementación (que vale la pena realizar para la refactorización conservadora del comportamiento)? –

Cuestiones relacionadas