Diseño de pruebas unitarias para un sistema con estado

20

Fondo

Test Driven Development se popularizó después de terminar la escuela y en la industria. Estoy tratando de aprenderlo, pero aún se me escapan algunas cosas importantes. Los defensores de TDD dicen muchas cosas como (en adelante denominado el "principio de afirmación única" o SAP ):

Durante algún tiempo he estado pensando en cómo las pruebas TDD pueden ser tan simples, tan expresivas y tan elegantes como sea posible. Este artículo explora un poco sobre cómo es hacer que las pruebas sean tan simples y descompuestas como sea posible: apuntando a una sola afirmación en cada prueba.

Fuente: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

También dicen cosas como esta (en adelante, el "principio del método privado" o PMP ):

Por lo general, no prueba los métodos privados directamente. Como son privados, considérelos un detalle de implementación. Nadie va a llamar a uno de ellos y esperar que funcione de una manera particular.

En su lugar, debe probar su interfaz pública. Si los métodos que llaman a sus métodos privados funcionan como espera, entonces asume por extensión que sus métodos privados funcionan correctamente.

Fuente: ¿Cómo prueba los métodos privados?

Situación

Estoy tratando de probar un sistema de procesamiento de datos con estado. El sistema puede hacer diferentes cosas para la misma pieza de datos dado cuál era su estado antes de recibir esos datos. Considere una prueba sencilla que desarrolle el estado en el sistema, luego pruebe el comportamiento que el método dado está destinado a probar.

  • SAP sugiere que no debería estar probando el "procedimiento de acumulación de estado", debería asumir que el estado es lo que espero del código de construcción y luego probar el cambio de estado que estoy tratando de probar

  • PMP sugiere que no puedo omitir este paso de "acumulación de estado" y simplemente probar los métodos que gobiernan esa funcionalidad de forma independiente.

El resultado en mi código real ha sido pruebas hinchadas, complicadas, largas y difíciles de escribir. Y si las transiciones de estado cambian, las pruebas tienen que cambiarse ... lo cual estaría bien con pruebas pequeñas y eficientes, pero extremadamente lento y confuso con estas pruebas largas e infladas. ¿Cómo se hace esto normalmente?

durron597
fuente
2
No creo que encuentres una solución elegante para esto. Para empezar, el enfoque general no es hacer que el sistema tenga estado, lo que no le ayuda cuando prueba algo que ya está construido. Refactorizarlo para que no tenga estado probablemente tampoco valga la pena el costo.
Doval
@Doval: explique cómo hacer que algo como un teléfono (SIP UserAgent) no tenga estado. El comportamiento esperado de esta unidad se especifica en el RFC utilizando un diagrama de transición de estado.
Bart van Ingen Schenau
¿Está copiando / pegando / editando sus pruebas o está escribiendo métodos de utilidad para compartir una configuración / desmontaje / funcionalidad común? Si bien algunos casos de prueba ciertamente pueden alargarse e hincharse, esto no debería ser tan común. En un sistema con estado, esperaría una rutina de configuración común donde el estado final sea un parámetro y esta rutina lo lleve al estado que desea probar. Además, al final de cada prueba, tendría un método de desmontaje que lo llevará de vuelta al estado de inicio conocido (si es necesario) para que su método de configuración funcione correctamente cuando comience la próxima prueba.
Dunk
En una tangente, pero también agregaré que los diagramas de estado son una herramienta de comunicación y no un decreto de implementación, incluso si está en un RFC. Siempre que cumpla con la funcionalidad descrita, cumple con el estándar. Tuve un par de ocasiones en las que convertí implementaciones de transición de estado realmente complicadas (como se define en RFC) en una funcionalidad de procesamiento general realmente simple. Recuerdo un caso al deshacerme de un par de miles de líneas de código una vez que me di cuenta de que, aparte de un par de banderas, alrededor de 5 estados hicieron exactamente lo mismo una vez que cambió el nombre de los elementos comunes "ocultos".
Dunk

Respuestas:

15

Perspectiva:

Entonces, demos un paso atrás y preguntemos con qué TDD está tratando de ayudarnos. TDD está tratando de ayudarnos a determinar si nuestro código es correcto o no. Y por correcto, quiero decir "¿cumple el código con los requisitos comerciales?" El punto de venta es que sabemos que se requerirán cambios en el futuro, y queremos asegurarnos de que nuestro código siga siendo correcto después de hacer esos cambios.

Traigo esa perspectiva porque creo que es fácil perderse en los detalles y perder de vista lo que estamos tratando de lograr.

Principios - SAP:

Si bien no soy un experto en TDD, creo que te estás perdiendo parte de lo que el Principio de Afirmación Única (SAP) está tratando de enseñar. SAP puede ser reexpresado como "prueba de una cosa a la vez". Pero TOTAT no se sale de la lengua tan fácilmente como lo hace SAP.

Probar una cosa a la vez significa que te enfocas en un caso; un camino una condición límite; un caso de error; uno lo que sea por prueba. Y la idea principal detrás de eso es que necesita saber qué se rompió cuando falla el caso de prueba, para que pueda resolver el problema más rápidamente. Si prueba varias condiciones (es decir, más de una cosa) dentro de una prueba y la prueba falla, entonces tiene mucho más trabajo en sus manos. Primero debe identificar cuál de los múltiples casos falló y luego averiguar por qué ese caso falló.

Si prueba una cosa a la vez, su alcance de búsqueda es mucho más pequeño y el defecto se identifica más rápidamente. Tenga en cuenta que "probar una cosa a la vez" no necesariamente lo excluye de mirar más de una salida de proceso a la vez. Por ejemplo, al probar un "buen camino conocido", puedo esperar ver un valor específico resultante, fooasí como otro valor bary puedo verificarlo foo != barcomo parte de mi prueba. La clave es agrupar lógicamente las comprobaciones de salida según el caso que se está probando.

Principios - PMP:

Del mismo modo, creo que te estás perdiendo un poco sobre lo que el Principio del Método Privado (PMP) tiene que enseñarnos. PMP nos anima a tratar el sistema como una caja negra. Para una entrada dada, debe obtener una salida dada. No le importa cómo el cuadro negro genera la salida. Solo le importa que sus salidas se alineen con sus entradas.

PMP es realmente una buena perspectiva para mirar los aspectos API de su código. También puede ayudarlo a determinar lo que tiene que probar. Identifique sus puntos de interfaz y verifique que cumplan con los términos de sus contratos. No necesita preocuparse por cómo los métodos detrás de la interfaz (también conocidos como privados) hacen su trabajo. Solo necesita verificar que hicieron lo que se suponía que debían hacer.


TDD aplicado ( para ti )

Por lo tanto, su situación presenta una pequeña arruga más allá de una aplicación ordinaria. Los métodos de su aplicación tienen estado, por lo que su salida depende no solo de la entrada, sino también de lo que se ha hecho anteriormente. Estoy seguro de que debería <insert some lecture>decir que el estado es horrible y bla, bla, bla, pero eso realmente no ayuda a resolver su problema.

Asumiré que tiene algún tipo de tabla de diagrama de estado que muestra los diversos estados potenciales y lo que debe hacerse para desencadenar una transición. Si no lo hace, lo necesitará, ya que ayudará a expresar los requisitos comerciales para este sistema.

Las pruebas: Primero, terminarás con un conjunto de pruebas que promulgan un cambio de estado. Idealmente, tendrá pruebas que ejerciten la gama completa de cambios de estado que pueden ocurrir, pero puedo ver algunos escenarios en los que es posible que no necesite llegar a ese punto.

A continuación, debe crear pruebas para validar el procesamiento de datos. Algunas de esas pruebas estatales se reutilizarán cuando cree las pruebas de procesamiento de datos. Por ejemplo, suponga que tiene un método Foo()que tiene diferentes resultados basados ​​en un estado Inity State1. Querrá usar su ChangeFooToState1prueba como un paso de configuración para probar la salida cuando " Foo()está en State1".

Hay algunas implicaciones detrás de ese enfoque que quiero mencionar. Spoiler, aquí es donde enfureceré a los puristas

En primer lugar, debe aceptar que está usando algo como prueba en una situación y una configuración en otra situación. Por un lado, esto parece ser una violación directa de SAP. Pero si usted se enmarca lógicamente ChangeFooToState1como teniendo dos propósitos, todavía está cumpliendo con el espíritu de lo que SAP nos está enseñando. Cuando necesite asegurarse de que los Foo()cambios cambien de estado, lo usará ChangeFooToState1como prueba. Y cuando necesite validar " Foo()la salida de cuando está en State1", entonces está utilizando ChangeFooToState1como configuración.

El segundo elemento es que, desde un punto de vista práctico, no querrá pruebas de unidad totalmente aleatorias para su sistema. Debería ejecutar todas las pruebas de cambio de estado antes de ejecutar las pruebas de validación de salida. SAP es una especie de principio rector detrás de ese pedido. Para decir lo que debería ser obvio: no puede usar algo como configuración si falla como prueba.

Poniendo todo junto:

Usando su diagrama de estado, generará pruebas para cubrir las transiciones. Nuevamente, utilizando su diagrama, genera pruebas para cubrir todos los casos de procesamiento de datos de entrada / salida controlados por estado.

Si sigue ese enfoque, las bloated, complicated, long, and difficult to writepruebas deberían ser un poco más fáciles de administrar. En general, deberían terminar siendo más pequeños y deberían ser más concisos (es decir, menos complicados). Debe notar que las pruebas también están más desacopladas o modulares.

Ahora, no estoy diciendo que el proceso será completamente indoloro porque escribir buenas pruebas requiere cierto esfuerzo. Y algunos de ellos seguirán siendo difíciles porque está mapeando un segundo parámetro (estado) en algunos de sus casos. Y además, debería ser un poco más evidente por qué un sistema sin estado es más fácil de crear pruebas. Pero si adapta este enfoque para su aplicación, debería descubrir que puede probar que su aplicación funciona correctamente.


fuente
11

Por lo general, abstraería los detalles de configuración en funciones para no tener que repetirlo. De esa manera, solo tiene que cambiarlo en un lugar de la prueba si la funcionalidad cambia.

Sin embargo, normalmente no desearía describir incluso sus funciones de configuración como hinchadas, complicadas o largas. Esa es una señal de que su interfaz necesita una refactorización, porque si es difícil de usar para sus pruebas, también es difícil de usar su código real.

Eso es a menudo una señal de poner demasiado en una clase. Si tiene requisitos con estado, necesita una clase que administre el estado y nada más. Las clases que lo soportan deben ser apátridas. Para su ejemplo SIP, el análisis de un paquete debe ser completamente sin estado. Puede tener una clase que analice un paquete y luego llame a algo como sipStateController.receiveInvite()administrar las transiciones de estado, que a su vez llama a otras clases sin estado para hacer cosas como sonar el teléfono.

Esto hace que la configuración de las pruebas unitarias para la clase de máquina de estado sea una simple cuestión de algunas llamadas a métodos. Si su configuración para las pruebas de unidad de máquina de estado requiere la elaboración de paquetes, ha puesto demasiado en esa clase. Del mismo modo, su clase de analizador de paquetes debe ser relativamente simple para crear un código de configuración, utilizando un simulacro para la clase de máquina de estado.

En otras palabras, no puede evitar el estado por completo, pero puede minimizarlo y aislarlo.

Karl Bielefeldt
fuente
Solo para que conste, el ejemplo de SIP fue mío, no del OP. Y algunas máquinas de estado pueden necesitar más que unas pocas llamadas a métodos para que estén en el estado correcto para una determinada prueba.
Bart van Ingen Schenau
+1 para "no puede evitar el estado por completo, pero puede minimizarlo y aislarlo". No pude estar de acuerdo. El estado es un mal necesario en el software.
Brandon
0

La idea central de TDD es que, al escribir las pruebas primero, terminas con un sistema que, al menos, es fácil de probar. Esperemos que funcione, se pueda mantener, esté bien documentado, etc., pero si no, bueno, al menos sigue siendo fácil de probar.

Entonces, si TDD y termina con un sistema que es difícil de probar, algo ha salido mal. Tal vez algunas cosas que son privadas deberían ser públicas, porque necesita que sean para pruebas. Quizás no estás trabajando en el nivel correcto de abstracción; algo tan simple como una lista tiene estado en un nivel, pero un valor en otro. O tal vez está dando demasiado peso a los consejos no aplicables en su contexto, o su problema es simplemente difícil. O, por supuesto, quizás tu diseño sea simplemente malo.

Cualquiera sea la causa, probablemente no va a volver y escribir su sistema nuevamente para hacerlo más comprobable con un código de prueba simple. Es probable que el mejor plan sea utilizar algunas técnicas de prueba un poco más sofisticadas, como:

soru
fuente