Comportamientos de prueba de unidad sin acoplamiento a detalles de implementación

16

En su charla TDD, donde todo salió mal , Ian Cooper empuja la intención original de Kent Beck detrás de las pruebas unitarias en TDD (para probar comportamientos, no métodos de clases específicamente) y argumenta a favor de evitar el acoplamiento de las pruebas a la implementación.

En el caso de un comportamiento como save X to some data sourceen un sistema con un conjunto típico de servicios y repositorios, ¿cómo podemos probar el almacenamiento de algunos datos a nivel de servicio, a través del repositorio, sin acoplar la prueba a los detalles de implementación (como llamar a un método específico )? ¿Evitar este tipo de acoplamiento en realidad no vale la pena el esfuerzo / mal de alguna manera?

Andy Hunt
fuente
1
Si desea probar que los datos se guardaron en el repositorio, entonces la prueba tendrá que ir y verificar el repositorio para ver si los datos están allí, ¿verdad? ¿O me estoy perdiendo algo?
Mi pregunta era más sobre evitar acoplar las pruebas a un detalle de implementación como llamar a un método específico en el repositorio, o realmente si eso es algo que debería hacerse.
Andy Hunt

Respuestas:

8

Su ejemplo específico es un caso que generalmente tiene que probar comprobando si se llamó a cierto método, porque saving X to data sourcesignifica comunicarse con una dependencia externa , por lo que el comportamiento que debe probar es que la comunicación se produce como se esperaba .

Sin embargo, esto no es algo malo. Las interfaces de límites entre su aplicación y sus dependencias externas no son detalles de implementación , de hecho, están definidas en la arquitectura de su sistema; lo que significa que tal límite no es probable que cambie (o si debe hacerlo, sería el tipo de cambio menos frecuente). Por lo tanto, acoplar sus pruebas a una repositoryinterfaz no debería causarle demasiados problemas (si lo hace, considere si la interfaz no le está robando responsabilidades a la aplicación).

Ahora, considere solo las reglas comerciales de una aplicación, desacopladas de la interfaz de usuario, las bases de datos y otros servicios externos. Aquí es donde debe ser libre de cambiar tanto la estructura como el comportamiento del código. Aquí es donde las pruebas de acoplamiento y los detalles de implementación lo obligarán a cambiar más código de prueba que el código de producción, incluso cuando no haya cambios en el comportamiento general de la aplicación. Aquí es donde las pruebas en Statelugar de Interactionayudarnos a ir más rápido.

PD: No es mi intención decir si la prueba por estado o interacciones es la única forma verdadera de TDD: creo que es una cuestión de utilizar la herramienta adecuada para el trabajo correcto.

MichelHenrich
fuente
Cuando menciona "comunicarse con una dependencia externa", ¿se refiere a las dependencias externas como aquellas que son externas a la unidad bajo prueba, o aquellas externas al sistema en su conjunto?
Andy Hunt
Por "dependencia externa" me refiero a cualquier cosa que pueda considerarse como un complemento para su aplicación. Por aplicación, me refiero a las reglas de negocios, independientes de cualquier tipo de detalle, como qué marco usar para persistencia o interfaz de usuario. Creo que el tío Bob puede explicarlo mejor, como en esta charla: youtube.com/watch?v=WpkDN78P884
MichelHenrich
Creo que este es el enfoque ideal, como dice la charla, para probar sobre una base de "característica" o "comportamiento", y una prueba por característica o comportamiento (o permutación de uno, es decir, parámetros variables). Sin embargo, si tengo 1 prueba "feliz" para una función, para hacer TDD, eso significa que tendré una única confirmación gigante (y revisión de código) para esa función, lo cual es una mala idea. ¿Cómo se evitaría esto? ¿Escribir una parte de esa característica como prueba y todo el código asociado con ella, luego agregar gradualmente el resto de la característica en confirmaciones posteriores?
jordania
Realmente me gustaría ver un ejemplo del mundo real de pruebas que se acoplan a la implementación.
PositiveGuy
7

Mi interpretación de esa charla es:

  • componentes de prueba, no clases.
  • probar componentes a través de sus puertos de interfaz.

No se menciona en la charla, pero creo que el contexto asumido para el consejo es algo como:

  • está desarrollando un sistema para usuarios, no, por ejemplo, una biblioteca o marco de utilidad.
  • El objetivo de las pruebas es entregar con éxito la mayor cantidad posible dentro de un presupuesto competitivo.
  • Los componentes se escriben en un lenguaje único, maduro, probablemente de tipo estático, como C # / Java.
  • un componente es del orden de 10000-50000 líneas; un proyecto Maven o VS, un complemento OSGI, etc.
  • Los componentes están escritos por un único desarrollador o un equipo estrechamente integrado.
  • estás siguiendo la terminología y el enfoque de algo como la arquitectura hexagonal
  • un puerto de componente es donde deja el idioma local, y su sistema de tipos, detrás, cambiando a http / SQL / XML / bytes / ...
  • En cada puerto se envían interfaces tipadas, en el sentido de Java / C #, que pueden tener implementaciones cambiadas para cambiar las tecnologías.

Por lo tanto, probar un componente es el mayor alcance posible en el que algo todavía se puede llamar razonablemente prueba unitaria. Esto es bastante diferente de cómo algunas personas, especialmente académicos, usan el término. No se parece en nada a los ejemplos del tutorial típico de la herramienta de prueba unitaria. Sin embargo, coincide con su origen en las pruebas de hardware; las placas y los módulos se prueban en la unidad, no los cables y tornillos. O al menos no construyes un Boeing simulado para probar un tornillo ...

Extrapolando de eso, y arrojando algunos de mis propios pensamientos,

  • Cada interfaz será una entrada, una salida o un colaborador (como una base de datos).
  • que probar las interfaces de entrada; llame a los métodos, afirme los valores de retorno.
  • te burlas de las interfaces de salida; verifique que se invoquen los métodos esperados para un caso de prueba dado.
  • que falsos los colaboradores; proporcionar una implementación simple pero funcional

Si lo hace de manera adecuada y limpia, apenas necesita una herramienta de burla; solo se usa unas pocas veces por sistema.

Una base de datos es generalmente un colaborador, por lo que se falsifica en lugar de burlarse. Sería doloroso implementarlo a mano; Afortunadamente, tales cosas ya existen .

El patrón de prueba básico es realizar una secuencia de operaciones (por ejemplo, guardar y volver a cargar un documento); confirmar que funciona Esto es lo mismo que para cualquier otro escenario de prueba; Es probable que ningún cambio en la implementación (en funcionamiento) provoque que tal prueba falle.

La excepción es cuando los registros de la base de datos se escriben pero nunca son leídos por el sistema bajo prueba; por ejemplo, registros de auditoría o similares. Estas son salidas, por lo que se deben burlar. El patrón de prueba es hacer alguna secuencia de operaciones; confirme que se llamó a la interfaz de auditoría con los métodos y argumentos especificados

Tenga en cuenta que incluso aquí, siempre que esté utilizando una herramienta de burla de tipo seguro como mockito , cambiar el nombre de un método de interfaz no puede causar una falla de prueba. Si utiliza un IDE con las pruebas cargadas, se refactorizará junto con el cambio de nombre del método. Si no lo hace, la prueba no se compilará.

soru
fuente
¿Me puede describir / dar un ejemplo concreto de un puerto de interfaz?
PositiveGuy
¿Cuál es un ejemplo de una interfaz de salida? ¿Puedes ser específico en el código? Lo mismo con la interfaz de entrada.
PositiveGuy
Una interfaz (en el sentido de Java / C #) envuelve un puerto, que puede ser cualquier cosa que hable con el mundo exterior (d / b, socket, http, ....). Una interfaz de salida es aquella que no tiene métodos con valores de retorno que provienen del mundo exterior a través del puerto, solo excepciones o equivalentes.
soru
Una interfaz de entrada es lo contrario, un colaborador es tanto de entrada como de salida.
soru
1
Creo que está hablando de un enfoque de diseño y un conjunto de terminología completamente diferentes a los descritos en el video. Pero el 90% del tiempo un repositorio (es decir, una base de datos) es un colaborador, no una entrada o salida. Y entonces la interfaz es una interfaz de colaboración.
soru
0

Mi sugerencia es utilizar un enfoque de prueba basado en estado:

DADO Tenemos el DB de prueba en un estado conocido

CUANDO se llama al servicio con argumentos X

ENTONCES Afirme que la base de datos ha cambiado de su estado original al estado esperado llamando a métodos de repositorio de solo lectura y verificando sus valores devueltos

Al hacerlo, no confía en ningún algoritmo interno del servicio, y es libre de refactorizar su implementación sin tener que cambiar las pruebas.

El único acoplamiento aquí es con la llamada al método de servicio y las llamadas al repositorio necesarias para leer los datos de la base de datos, lo cual está bien.

Elifarley
fuente