Flow expectation test como parte de tus unit test.

En mi entrada sobre porque usar la librería Rhino Mock para unit test explico brevemente su utilidad para testear el “qué” hace un método o tu SUT (Sistem under test). A este “probar el qué” hace una función, sin entrar a los detalles técnicos sobre como cumple con su función se le puede conocer como test de flujo esperado o flow expectation test. En esta entrada te hablo sobre sus ventajas y su importancia.Con un test de flujo de llamadas simplemente estarás testeando que todas las llamadas que esperas que se realicen, son realizadas, sin evitar ninguna de las esperadas, sin realizar ninguna no esperada y realizándolas todas en el orden esperado.

Esta forma de testear es especialmente útil en capas de servicio, donde únicamente deberías tener llamadas a métodos de dominio, de forma que todas estas llamadas, realizadas una tras otra, cumplan con los criterios esperados por la lógica de negocio.

Cabría esperar ante una operación de creación de una entidad unos criterios como:

  • Validar la seguridad: El usuario puede realizar la acción de creación de la entidad
  • Validar el objeto: Todos los criterios de validación, como longitudes, campos requeridos, estados, auditoria… son correctos.
  • Mapear el objeto: Si estas trabajando con viewmodels, necesitarás convertir tu objeto a tu modelo de persistencia de datos.
  • Persistir el objeto: Realizar la operación de escritura a través de tu capa de persistencia.

En la entrada sobre Rhino Mock que también te comento al principio, puedes ver un ejemplo de como quedaría un test de flow expectation test para un método de servicio que sigue estas pautas. Aunque aquí te voy a mencionar otros ejemplos con algo mas de profundidad.

Diferencias

Con este sencillo ejemplo podrás ver las diferencias de código entre aplicar una arquitectura con capa de servicio que incluya toda la lógica, y otra arquitectura donde la capa de servicio solo incluya llamadas a otros métodos con una capa de dominio diferenciada.

Si todas estas operaciones las realizamos en un solo método de servicio, podrías tener algo como lo siguiente (perdona si el código no es muy limpio o no esta refactorizado todo lo que se podría, esta improvisado para servir como ejemplo de mala práctica):

Como puedes ver el código no es fácil de seguir.

Si en cambio lo refactorizas para tener solo llamadas, podrías tener algo como esto:

En este caso puedes apreciar como se obtiene el usuario con login en la app, sus roles, los roles actuales del usuario del que se pretenden actualizar los roles, se valida si el usuario con login puede editar los roles indicados para el usuario especificado, se valida el modelo, se actualizan los roles y se persisten los cambios. Este código, a diferencia del anterior, se puede seguir de un solo vistazo.

Ventajas de usar flow expectation test

Menor reworking y refactoring

Necesitarás hacer menos reworking ante cambios en al lógica, a continuación te indico el razonamiento.

En el primer caso necesitarás una batería con cierto número de unit test para poder dotar a tu código de una cobertura de 100%, pero si tu cliente te solicita un cambio en la lógica, además de cambiar el código en tu método, necesitarás también refactorizar un número elevado de esos unit test (depende del cambio).

En el segundo caso solo necesitarás un test de verificación del flujo de llamadas, y si el cliente te solicita un cambio en la lógica, solo necesitarás cambiar el contenido de alguno de los métodos que estas utilizando de forma aislada, siendo menor el número de unit test que necesitarás refactorizar, ya que solo afectarán a ese método al que llamas, y no al resto de métodos, probados de forma aislada.

O si el cambio no aplica a ninguno de tus métodos existentes, necesitarás introducir una nueva llamada a un nuevo método, con lo que únicamente necesitas refactorizar el test de flujo de llamadas, siendo solo un test a ser refactorizado (además de crear los nuevos test para ese nuevo método, algo que no es una tarea de reworking).

Test de la lógica de negocio

Con un test de flujo de llamadas estarás probando que tus llamadas son realizadas como esperas, independientemente de cómo hayas resuelto el apartado técnico, por lo que es muy fácil comprobar si tus criterios de aceptación impuestos por tu cliente se están realizando, ya que verificas las llamadas necesarias para ello.

Facilidad de mantenimiento

Si planteas tus servicios para que puedan soportar solo llamadas, sin lógica, de cara a poder hacer este tipo de test, tu código prácticamente se autodocumentará él solito, cualquier desarrollador que necesite introducir cambios en tu código, podrá solo leyendo las llamadas existentes en tu servicio, hacerse una idea de cual es el comportamiento de ese servicio, prueba de ello son los ejemplos que te he presentado con anterioridad. ¿Cuál te resulta más fácil de seguir e identificar su funcionamiento? ¿ Cuál te resultaría mas fácil de adaptar o modificar?

Desacople con las demás capas

Otra ventaja de plantear este tipo de arquitectura en tu capa de servicio, es que por encima podrás tener una webApi, el controlador de una app MVC, un WCF, los endpoints de un microServicio de Service Fabric… da igual, sea cual sea tu tecnología de comunicación con otros sistemas podrás ubicar esta capa que contiene toda la lógica de negocio por debajo, y además, como tiene solo llamadas a través de interfaces, podrás aislarte de la implementación en la capa de dominio, por lo que puedes modificar por completo la implementación utilizada para resolver el problema, siendo conocedor de que el problema siempre debe ser resuelto con las llamadas existentes, ofreciendo ciertos outputs ante determinados inputs.

Desventajas de usar flow expectation test

Pueden ser test que no sean fáciles de leer

Si en tu capa de servicio, para un método determinado, estás utilizando llamadas a varias clases diferentes para llamar a diferentes métodos, tu test de flow expectation puede ser complejo de hacer o de leer y modificar ante la introducción de una nueva llamada. A la larga se consigue práctica, como con todo, pero de entrada para personas nuevas en Unit Test puede parecer poco intuitivo.

Esta desventaja puede solventarse con una capa intermedia en la arquitectura, de forma que si desde tu servicio necesitas llamar a clases como securityClass, mapperClass, validationClass, repositoryClass, etc, introduzcas una capa intermedia (por ejemplo domainClass) que se encargue de conectar tu capa de servicio con cada una de estas capas, a modo de proxy, de forma que si tu servicio solo llama a domainClass.security, domainClass.mapper, domainClass.validation, domainClass.repository, etc, tus test de flujo serán infinitamente más fáciles de hacer, leer y modificar.

A continuación te presento dos ejemplos para que aprecies esta diferencia.

Seguro que puedes seguirlo pero con cierto detenimiento y esfuerzo, si no es así, te dejo esta otra entrada donde explico cómo funciona, y a continuación te dejo el ejemplo de test de flujo con una capa de dominio intermedia a modo de proxy.

¿Mucho mejor verdad? En esta entrada explico como funciona este otro caso.

¿Qué estas testeando?

Lo que estos test están probando, en los dos ejemplos anteriores, es lo mismo, verificamos que las llamadas que nuestra capa de servicio hace a las clases con las que conecta, son realizadas, en ese orden, sin existir ninguna intermedia no incluida en las verificaciones, ni verificar una que no exista en la implementación.

Por lo tanto, si algún desarrollador añade una nueva llamada a otro método, en cualquier posición, o cambia el orden de estas llamadas, o incluso suprime alguna de ellas, el test fallará. Esto debe entenderse como una ventaja, ya que la situación de lógica de negocio que el test pretendía representar ya no existe, no es representativo de lo que está ocurriendo en el SUT (System under test), y por tanto debe ser refactorizado ante la nueva situación que se esta produciendo tras el cambio en la implementación. Digamos que estas “blindando” tu lógica a nivel de servicio para que si se produce un cambio, este deba ser intencionado y ratificado con una refactorización del test, no va a ser algo “accidental”.

¿Qué no estas testeando?

Unit test

No estas testeando que lo que hace cada una de estas llamadas que verificas que suceden, se este comportando como se espera, por lo tanto será necesario que apliques unit test a cada uno de los métodos que están siendo llamados, de forma que si estos test siempre se comportan como se espera ante determinados inputs, ofreciendo ciertos outputs o cierto comportamiento, tendrás muy alta probabilidad de que tu aplicación este funcionando con éxito.

Integration test

Los unit test te dan un alto porcentaje de éxito, y tendrás tu código en un % de code coverage (código bajo la cobertura de pruebas) muy bueno. ¿Pero qué pasa si se te ha pasado validar para algún unit test una cierta casuística, para lo que no estás validando que se produzca cierto output ante un determinado input, que a su vez es importante para el siguiente método del flujo, ya que condicionará su comportamiento? Aunque en este otro método si estés considerando ese unit test para el cierto output del anterior método, tu código podría no funcionar si esa casuística específica no funciona como se espera de ella. Para que este tipo de errores salten a la luz, es para lo que es necesario realizar integration test.

Estos test no aplican mocks entre las diferentes capas, solo mockean las llamadas a servicios externos o bases de datos (mockeando repositorios), de forma que podrás tener un test que garantiza que ante una entrada en tu controlador, la respuesta es la esperada (para ser devuelta a tu cliente) ante ciertas suposiciones de que los sistemas externos se comportan de acuerdo al stub que defines en el mock, simulando su comportamiento. Pese a esta simulación de sistemas externos, tu código estará pasándose los objetos manipulados entre las diferentes capas, por lo que tendrás una muy muy alta probabilidad de que tu sistema funcione perfectamente.

Sobre estos integration test hablaré en entradas futuras, por otro lado si quieres saber más sobre que es un mock o un stub, hablo de ello en esta entrada.

Espero que la explicación y los ejemplos te sean de utilidad para mejorar tu skill de unit testing, desarrollando código de calidad, limpio y del que podrás estar seguro gracias a la ayuda de los test que pasan en verde.

Un saludo.

 

Deja un comentario