Model based testing mediante Graphwalker

Introducción

Llevo un tiempo investigando nuevas formas para definir y diseñar las pruebas en nuestro equipo. La idea principal es disponer de un modelo de diseño de pruebas que sirva como punto de encuentro entre el rol de producto, desarrollador y QE.

En este post vamos a hablar sobre Model-based testing o MBT que se nos ayuda a acercarnos bastante a la idea que he comentado arriba.

Y como siempre, vamos a verlo en práctica, para ello vamos a hacer uso de Graphwalker, una herramienta de MBT.

Podéis descargar el proyecto desde mi Github. Esta vez, veréis que el proyecto únicamente tiene logs para ver el flujo de secuencia del test. Usarlo como base y experimentad!

Model based testing (MBT)

En Model-based testing (MBT) se define nuestro SUT en base a vértices o vertex y la transición o edge entre estos.

model based testing example

El modelo, se compone de tres vértices o vertex:

  • v_App_closed, como vértice inicial y final.
  • v_App_running.
  • v_Display_preference

De tal forma que en el caso de este modelo (que es sencillo) si queremos probar el 100% de los vértices y las transiciones, el test path más lógico sería:

Que para llegar de uno a otro requiere de ciertas transiciones o edge:

  • e_Start_app, transición inicial.
  • e_Close_app.
  • e_Open_preferences.
  • e_Close_preferences.

De tal forma que en el caso de este modelo (que es sencillo) si queremos probar el 100% de los vértices y las transiciones, el test path más lógico sería:

mtb example flow

Como hemos dicho, nuestro caso es muy sencillo y es fácil identificarlo, pero lo normal es tener modelos mucho más complejos.
Para estos casos es cuando las herramientas de MBT como Graphwalker, nos ayudan a generar los test path.

Normalmente las herramientas MBT nos proporcionan dos formas de generar los test path.

OFFLINE

En la generación offline, los test path se generan previamente, de tal forma que puedan ser ejecutados con posterioridad por la herramienta.

ONLINE

En la generación online, los test path se generan automáticamente en tiempo de ejecución. Para este caso, la herramienta o el framework de MBT estará acoplado al código del test, pero esto nos aporta ciertas ventajas.

Vamos a verlo en la práctica!

Añadir Graphwalker a nuestro proyecto

Para añadir Graphwalker a nuestro proyecto, únicamente necesitamos añadir las dependencias y el plugin:

Lo veremos más adelante, pero mediante maven generamos las interfaces que tendrán que implementar nuestras clases.

Diseñando el modelo

Para diseñar el modelo vamos a usar yEd, que nos permitirá diseñar el modelo gráficamente, siendo totalmente compatible con Graphwalker.

Imaginemos que tenemos una aplicación, de venta de entradas. Formamos parte del equipo de pagos y tenemos una arquitectura basada en microservicios.

La idea es probar la feature de la compra de una entrada a través de las APIs.

mbt purchase

Nuestro modelo se va a componer de cinco vertex:

Crear usuario -> Añadirle el medio de pago -> Crear el carrito de compra del usuario -> Añadir la entrada al carrito de compra -> Comprar el ticket

Si nos fijamos en los edges entre los vértices, podemos ver pueden ir acompañados de “cierta lógica”.

Es muy importante nombrar los edged, con una “e” por delante y los vertex con una “v”, de esta forma Graphwalker podrá identificarlos.

mbt exa 1

Por ejemplo, en el caso del edge e_AddPaymentMethod, vemos el código:

/paymentMethod=’paypal’; validPaymentMethod=true;

Esto quiere decir que Graphwalker, cuando vea el símbolo /, todo lo que venga posteriormente lo va a interpretar como código javascript, es decir, en esta transición vamos a setear valor para dos variables.

mbt ex 2

Si nos fijamos en el siguiente edge, vamos a ver un código un poco diferente:

[validPaymentMethod]

Graphwalker cuando vea los símbolos […], lo va a interpretar como un condicional, es decir, va a comprobar que el valor de validPaymentMethod sea true.

Al ser código javascript, tanto el código como las condiciones pueden ser mucho más complejas, depende de las necesidades de nuestras pruebas.

Os dejo un enlace de la documentación de Graphwalker donde podreis verlo con mucho más detalle.

GENERANDO LA INTERFAZ DEL TEST

Para generar la interfaz del modelo para nuestras pruebas, vamos a guardar el fichero graphml de yEd en el directorio src/test/resources/ del proyecto.

Ejecutamos el comando de maven:

mvn graphwalker:generate-test-source

Esto nos generará en el directorio target la interfaz que tendremos que implementar en nuestras pruebas para lanzarlo con Graphwalker:

interface

Desarrollando y ejecutando las pruebas

En el test definiremos el algoritmo con el que queremos que se genere la secuencia de nuestro test.  La interfaz nos indicará los métodos de los que se compone nuestro test.

En el test definiremos el algoritmo con el que queremos que se genere la secuencia de nuestro test. En nuestro caso:

@GraphWalker(value = "quick_random(edge_coverage(100))", start = "e_CreateUser")

  • quick_random: Ejecutará el camino más corto, lo va decidir mediante el algoritmo de Dijkstra.
  • edge_coverage: Que ejecute el 100% de los edges que hemos definido en el modelo.

Es decir, en nuestro caso va a ejecutar todos los edges, y por cada vuelta va calcular cual es el camino más rápido.

Esta parte es la más interesante e importante, debido a que depende el algoritmo de ejecución que definamos probaremos más o menos, es decir, decide nuestra estrategia de testing.

Os dejo un enlace donde podéis ver las diferentes opciones de ejecución.

Para ejecutar las pruebas:

mvn graphwalker:test

test results

Como resultado vemos que se han ejecutado el 100 de los vertex y los edges, por lo que hemos lanzado pruebas con una cobertura del 100%.

Conclusiones

Definir las pruebas con MDT puede ayudarnos a alinearnos mejor con producto y con desarrollo, dado que los modelos clarifican bastante el flujo de testing de nuestro SUT.

El poder integrarlo con herramientas como Graphwalker en donde podemos definir el algoritmo de ejecución de las pruebas, nos aporta una gran flexibilidad a nuestra estrategia de testing.

Imaginemos que queremos hacer smoke testing, bastaría con definir la ejecución como:

e_start(reach_vertex())

Y si por ejemplo queremos hacer pruebas de estabilidad podemos añadirle tiempo de duración a nuestras pruebas:

random(time_duration())

Otro de los puntos fuertes, es que no está casado con ningún tipo de testing, es decir, al definir únicamente el flujo o los pasos del test, podemos realizar desde test de UI mediante selenium, a APIs, performance etc.

Aislando nuestras pruebas con Testcontainer

Introducción

Hace unos días escribí sobre las pruebas aisladas e integradas. Uno de los mayores retos en nuestra estrategia de testing es el hecho de intentar aislar nuestras pruebas.

Hemos visto cómo aislarlas mediante contract testing, pero; ¿Cómo aislamos nuestras pruebas de integración o las funcionales?

En este post vamos a hablar sobre Testcontainer, una librería de java que nos provee instancias ligeras de cualquier contenedor de Docker (bases de datos, navegadores, servicios etc.).

Como siempre vamos a ver los ejemplos con un proyecto que podréis descargar desde GitHub.

Vamos a ello!

Aislando las pruebas del proyecto

App architecture
El proyecto de ejemplo es sencillamente una aplicación conectada a una base de datos MySQL y que consume a encoder-service,  un servicio propio, que es usado por todas nuestras aplicaciones.

Vamos a desarrollar pruebas de integración. Comprobaremos que nuestra aplicación comunica correctamente tanto con la base de datos, como con el servicio.

Mediante Testcontainer cuando ejecutemos las pruebas, se levantará un contenedor Docker de MySQL y otro de encoder-service.

Una de las preguntas que os podéis hacer, es el porqué levantar un contenedor del servicio, si lo podemos mockear. Hay ocasiones en las que cuando realizamos pruebas, necesitamos recibir un comportamiento real, y no uno esperado. Este ejemplo pretende acercarse a esa realidad.

Test architecture

Añadiendo Testcontainer al proyecto

Vamos a añadir dos dependencias a nuestro proyecto. Las dependencias generales de Testcontainer, y la dependendencia de testcontainer de MySQL.

Además de añadir las dependencias de Testcontainer, deberemos tener las dependencias del conector de MySQL y Spring data.

Pruebas de integración sobre MySQL Testcontainer

Lo bueno de utilizar Spring Data JPA,  es que nos facilita la implementación y por tanto el uso de la capa de datos. Testcontainer se adapta muy bien a Spring Data JPA debido a que nos provee un driver jdbc que gestiona el contenedor de base datos.

Lo que nos interesa es ejecutar el contenedor de MySQL en tiempo de test, por lo que la configuración la vamos a añadir en el directorio test.

En la configuración de JPA, es importante destacar la propiedad de ddl-auto. El valor que vamos a indicarle es create-drop, de esta forma cuando se instancie el contenedor, se creará la base de datos con nuestra entidades.

Para las pruebas vamos a añadir unos datos iniciales a la base de datos, por lo que cuando se creen las tablas, se ejecutará el fichero data.sql. Para ello es importante que la propiedad initialization-mode tenga el valor always, en la configuración de datasource.

Lo más importante es driverClassName, donde indicaremos el driver de Testcontainer. En la propiedad de la url de conexión, el hostname, el puerto y el nombre de la base de datos van a ser ignorados.

El test que es muy sencillo, únicamente comprueba que se almacena y se leen los datos de la base de datos. Lo importante es sacar la conclusión de que el uso de testcontainer es totalmente transparente para el test, es decir, el test no sabe que está ejecutando las pruebas contra un contenedor de MySQL. Por lo que sí tenemos la necesidad de cambiar de base de datos o de versión de la base de datos, nuestras pruebas no se van a ver afectadas.

testcontainer results

 

Pruebas de integración sobre enconder-service Testcontainer

Primero, hemos tenido que “dockerizar” el encoder-service y añadirlo a nuestro local registry. Ahora en la configuración test de nuestra aplicación vamos a añadir el fichero docker-compose, con la configuración mínima para instanciar el contenedor de nuestro servicio.

En este caso, nuestro test si va a estar acoplado a las dependencias de Testcontainer, debido a que tenemos que hacer uso de docker compose.

Como hemos comentado, dado que queremos instanciar el contenedor de nuestro servicio desde el fichero de docker-compose.yml, vamos hacer uso de DockerComposeContainer, indicando el nombre del servicio y el puerto. En el test, obtenemos la url y el puerto del contenedor mediante getServiceHost y getServicePort. Realmente es muy sencillo.

testcontainer result encoder service

Conclusiones

Testcontainer nos permite aislar nuestras pruebas de forma muy sencilla. Mediante Spring data JPA y Testcontainer nuestras pruebas de integración de base de datos estarán totalmente desacopladas.

La mayor pega puede ser el hecho de que si queremos hacer uso de Docker compose, nuestras pruebas no van a disfrutar de la virtud de estar desacopladas de Testcontainer.

Al aislar las pruebas reducimos el tiempo de prueba, debido a que no necesitamos hacer conexiones reales y reducimos el número de falsos positivos por fallos en el otro extremo.

El sistema de CI/CD se puede ver muy beneficiado, dado que no vamos a necesitar desplegar todos estos servicios, BBDD etc. en nuestro entorno, por lo menos en entornos previos al stage o preproducción, y los desarrolladores podrán probar con mayor fiabilidad su código.

Además, Testcontainer nos proporciona librerías para hacer pruebas de UI, por ejemplo con Selenium. Y bueno… como habéis visto en el caso de encoder-service, cualquier aplicación dockerizada se podrá instanciar con testcontainer.

Testing with sewage treatment plant

Imagine that an issue in development is like water that has to be purified, and our testing architecture or techniques are the purification machine.

If we do not purify or filter, the water will come with a big amount of waste. In order to have clean water to consume, it must go through a process of “filtering” step by step.

filter

The same happens with the issue that we are developing. If we do not apply testing techniques and clean code, we will obtain a product with many residues (bugs, corner cases, dirty code, etc).

But we have to apply them from an early stage. So it is not correct doing testing once the issue is developed. The testing starts from the very beginning, defining the acceptance criterias, applying the different testing techniques during the development process.

In this way we will detect the “imperfections” in advance, and the delivery and deployment will be safer and faster avoiding the testing bottleneck.

A bit of dirty water can contaminate the entire pool in production, after that, ¿How could you find the drop that spoiled it all?

Pruebas aisladas e integradas ¿Qué son?

Introducción

Leyendo sobre estrategias de testing de microservicios en entornos PaaS, me he encontrado con un artículo sobre una charla bastante interesante que creo que tengo que compartir.

http://blog.thecodewhisperer.com/permalink/integrated-tests-are-a-scam

El post habla sobre el concepto de las pruebas integradas (que no de integración) y según J.B Raisemberg porque él cree que son una farsa. Sin duda, no podéis dejar de verlo!

En este post, analizaré dos de los conceptos que se tratan, las pruebas aisladas (isolated tests) y las pruebas integradas (integrated tests).

Qué son las pruebas integradas?

J.B Raisemberg define las pruebas integradas como:

“I use the term integrated test to mean any test whose result (pass or fail) depends on the correctness of the implementation of more than one piece of non-trivial behavior.”

tests results

Imaginemos que nuestro SUT (sistema bajo test) está compuesto por dos servicios que dependen de uno tercero. Lanzamos una suite de pruebas funcionales y como resultado obtenemos que varias de las pruebas han fallado.

 

Analizándolas vemos que son fallos debidos a la comunicación en el otro servicio.

Esto sería un ejemplo de que el resultado de nuestras pruebas funcionales dependen de otros componentes no triviales, por lo que estaríamos hablando de pruebas integradas.

En las pruebas de integración puede ocurrir lo mismo. En muchas ocasiones para poder validar una acción o una respuesta, es mayor la preparación de los componentes dependientes que la aserción del resultado. Bien es cierto, que esto puede mostrar un mal diseño de la prueba.

integrated pyramid

 

Si lo vemos bajo la pirámide tradicional del testing, las pruebas integradas serían todas las que no son unitarias, dado que tanto las de integración como las funcionales van a depender de componentes “externos” a la prueba en cuestión.

Por otro lado, las pruebas integradas aumentan considerablemente la complejidad del testing.

 

integrated paths

Supongamos que en nuestro SUT (sistema bajo test) tenemos tres componentes, con 5, 2 y 3 paths a probar. Si queremos probar toda la casuística combinatoria deberíamos tener 5*2*3 = 30 pruebas. En el caso de añadir un nuevo componente, por ejemplo con 2 paths llegaríamos a las 60 pruebas!

 

Ya no solo es que el coste de mantenimiento de la suite de pruebas sea alto, es que si uno de los componentes tiene un bug, afectará a todo el conjunto de pruebas y los falsos positivos se multiplican.

El coste de mantenimiento y la complejidad de las pruebas integradas como vemos puede llegar a ser exponencial O(n!).

complexity

Para los proyectos en los que estoy trabajando me estaba planteando hacer uso de machine learning, hacer un sistema para clasificar los errores bajo ciertos patrones, debido a que tenemos una cantidad importante de falsos positivos debido a entorno etc.

Pensando de forma objetiva, montar un proyecto de tal calibre para clasificar los fallos requiere de un gran esfuerzo, y en este caso no soluciona nada, solo enmascara los problemas: el diseño de las pruebas, el entorno y la estrategia de testing. Y todo porque disponemos de una suite compleja en la que la mayoría de las pruebas son integradas.

Y… ¿Las aisladas?

Cuando las pruebas sobre un componente no requieran de la integración con otro componente no trivial, hablaremos de pruebas aisladas.

isolated pyramid Es el caso de las pruebas unitarias, que se hacen de forma aislada para comprobar el correcto funcionamiento de nuestro código, simulando el comportamiento o la respuesta de ciertas partes u objetos del código.

 

 

 

 

lineal

El coste de mantenimiento y la complejidad de las pruebas aisladas es lineal O(n), ya el número de pruebas depende del número de paths que tengamos que probar por cada componente.

 

Si haríamos pruebas aisladas para el caso anterior, tendríamos en total 5 + 3 + 2 = 10 pruebas y no 30 como en el caso de las integradas.

Desacoplar y aislar las pruebas integradas

Las pruebas unitarias son aisladas por naturaleza (siempre que estén bien diseñadas), pero claro, en nuestro proyecto muy probablemente tengamos pruebas unitarias, de integración, funcionales etc.

¿Pero, cómo podemos convertir las pruebas integradas en aisladas?

Bueno pongámonos en caso anterior, en el que nuestro SUT está compuesto por dos servicios que se comunican entre sí y además dependen de uno tercero. Para poder probar los servicios de forma aislada debemos simular todas las respuestas esperadas.

integrated_aislated

En la imagen de abajo, vemos que hemos aislado las pruebas de los componentes mediante contract testing. De esta forma, en el lado del servicio validamos que este responde como tiene que responder a las diferentes peticiones, y que nuestro componente frente a una respuesta concreta reacciona como debería.

integrated_aislated

El ejemplo se ha mostrado con microservicios, pero en el caso de querer aislar los componentes de una aplicación monolítica se podría seguir una aproximación, al igual que para las pruebas de integración, como bien explica Raisemberg en su video.

pyramid

Finalmente, si echamos la mirada atrás a la pirámide tradicional del testing, vamos a ver que ahora el scope de las pruebas integradas se ha reducido. Si bien es cierto que podemos aislar ciertas pruebas funcionales, no podemos aislarlas todas.

 

 

 

 

 

Conclusiones

Creo que es importante pensar en un diseño de pruebas aisladas en la estrategia que sigamos en nuestros proyectos.

Puede que el diseño o la arquitectura de nuestro SUT parezca más compleja o que requiera de una mayor tecnicidad pero tenemos que valorarlo frente a las ventajas que nos ofrece el aislar nuestras pruebas:

  • Reducir el total de las pruebas.
  • Reducir los falsos positivos, mejorando así el análisis de los resultados.
  • Mejorar la mantenibilidad de la suite de pruebas.
  • Reducir la complejidad de las pruebas.
  • Reducir el tiempo de ejecución de las pruebas, debido a que las pruebas integradas dependen de comunicar con otros componentes (que a su vez pueden comunicar con otros) y por tanto de su tiempo de respuesta.

A parte de las ventajas que nos aporta el uso de contract testing, como podéis leer en un post en este mismo blog.

Pruebas de rendimiento y rastreo mediante Spring Cloud Sleuth y Zipkin

Introducción

Siguiendo por el hilo del post anterior en este artículo me gustaría tratar sobre el tracing de los servicios. Cuando hablamos de “tracear” los microservicios, estamos hablando de rastrear todas las llamadas y respuestas que se hacen entre los diferentes servicios de nuestro sistema.

Tener un control sobre cada llamada es muy importante ya que nos aporta una visibilidad en tiempo real del estado de cada petición y respuesta que se da, de tal forma que no perdemos ningún detalle de lo que ocurre (tipos de peticiones y respuestas, tiempos y errores) entre nuestros servicios.

En nuestro caso vamos a hacer las pruebas con zipkin como sistema de rastreo y para nuestros servicios vamos a hacer uso de Spring Cloud Sleuth que es la herramienta que nos aporta Spring Cloud para instrumentar sus servicios.

Podéis descargar el proyecto de pruebas como siempre desde mi GitHub.

He de decir que cuando desarrollé el proyecto de pruebas lo hice sobre la versión 1.3 de Sleuth, ahora se encuentran en su versión 2.0. Si queréis migrarlo os dejo el siguiente enlace.

No solo me gustaría centrarme en el uso de Zipkin o Sleuth, si no, que este post viene dado por un debate que tuve con mis compañeros de equipo (y que luego trasladé a Federico Toledo), sobre la mejor estrategia para probar el rendimiento de los servicios públicos e internos.

Entendamos públicos como aquellos que están abiertas al consumo exterior e internas como aquellas que solo son consumidos por otros servicios.

Mi opinión es que el mejor approach era realizar las pruebas de rendimiento sobre los servicios públicos, ya que estos internamente iban a llamar a los servicios internos, siempre y cuando tendríamos alguna forma clara de rastrear y obtener los datos de los tiempos y el rendimiento de los servicios internos.

Bien, ¿Y qué tal si usamos zipkin y sleuth y así vemos un ejemplo de esto mismo? Vamos a ver qué conclusiones sacamos.

De forma sencilla, como funciona Sleuth y Zipkin

Veamos un ejemplo del rastreo de una petición para ver y entender la información recogida. En la imagen de abajo vemos dos trazas. Las trazas están compuestas por spans (lo veremos ahora) y muestran la información del “recorrido” de la petición, es decir, lanzando una petición, porque servicios hemos pasado.

Traza zipkin
En este caso hemos lanzado una petición a purchase-service que internamente ha llamado a inventory y account-service. Si accedemos a la información de la traza, lo vemos con más detalle.

Lo que vamos a ver son los spans, es decir, la unidad de información que contiene los datos de una request/response, los tiempos etc. Vamos a tener un span por cada petición y respuesta que se haya dado dentro de la traza. Cada traza va a tener asociados una serie de spans, por lo que en nuestro caso los spans A, B y C estarán asociados a la traza T.

Zipkin spans

span info
Vamos a acceder al span de inventory-service para ver su información con más detalle:

Las anotaciones son los registros de los eventos de la petición. En este caso se puede ver los tiempos en los que el cliente ha realizado la petición (CS), esta se ha recibido (SR), se ha respondido (SS) y cuando el cliente ha obtenido la respuesta (CR).

Por otro lado, en la tabla inferior vemos información más detallada sobre la petición en cuestión. La key get-inventory-item y su valor se ha definido en la instrumentación del código, para aportar mayor información en el span.

Si hubiera algún error, veríamos los detalles del error en este punto:

zipkin error

Dado que creo que es un tema muy interesante, os recomiendo el siguiente enlace enlace a la documentación de sleuth, donde se profundiza mucho más en la terminología y en cómo trabaja con zipkin.

Entendiendo el proyecto de pruebas

 

microservice

El proyecto con el que vamos a trabajar consiste en tres servicios: Purchase, Inventory y Account.

Cuando llamemos a la API de purchase-service para comprar un item esta llamará a inventory-service y account-service para obtener los datos necesarios para la compra.

Vamos a instrumentar las APIs de los tres servicios y atacaremos a purchase-service, dado que internamente llamará a los otros servicios.

Si queréis investigar sobre el proyecto de pruebas, veréis que Inventory-service también expone una API que llama internamente a account-service para obtener información.

Preparando el proyecto

Lo primero que vamos a necesitar es instalar zipkin en nuestro equipo. Yo recomiendo hacer uso de docker e instalar la imagen de zipkin. Os dejo un enlace para hacerlo.

Ahora tenemos que configurar nuestro proyecto e instrumentar las APIs para rastrear las llamadas. Lo primero es añadir las dependencias necesarias en nuestro build.gradle (en nuestro caso haremos uso de gradle).

Vamos a añadir la configuración para conectar a nuestro zipkin local. Para ello en el fichero application.properties de src/main/resources añadimos las siguientes propiedades:

spring.zipkin.baseUrl: Url de nuestro zipkin.

spring.sleuth.sampler.percentage: La cantidad de peticiones que se rastrean. Por defecto es el 10% (0.1), como en nuestro caso no vamos a saturar el sistema ya que es un proyecto de pruebas vamos a rastrear el 100% de las peticiones.

sample.zipkin.enabled: Cuando lo tenemos a true, las trazas se envían a zipkin, en nuestro caso es lo que queremos. En caso contrario se enviarán a la consola de logs.

Vamos a ver un ejemplo básico de como instrumentamos la API de purchase. Vamos al controlador y al método purchase que será donde accedamos cuando llamemos a /purchase. Creamos un nuevo tag, añadiendo información que nos resulte interesante de rastrear.

Haremos lo mismo para inventory-service y para account-service:
https://github.com/aaguila/spring-cloud-sleuth-zipkin-example/blob/master/inventory-service/src/main/java/com/qajungle/controllers/InventoryController.java

https://github.com/aaguila/spring-cloud-sleuth-zipkin-example/blob/master/account-service/src/main/java/com/qajungle/controllers/AccountController.java

En este caso la información que metemos en el tag tiene menos sentido debido a que en la petición ya tenemos el id. Para la versión 2.0  la forma de gestionar los spans cambia, ya que ahora en vez de usar Sleuth hace uso de Brave. Podéis verlo en la guía de migración.

Con la configuración básica que hemos añadido al proyecto ya podríamos rastrear las peticiones. Vamos a ver qué resultado obtenemos.

Rastreando las pruebas de rendimiento

Lo primero que vamos a hacer es arrancar zipkin para que comience a rastrear las peticiones de nuestros servicios. Para ello basta con arrancar el docker y ejecutar el comando:


docker run -d -p 9411:9411 openzipkin/zipkin

Para nuestro ejemplo vamos a configurar nuestras pruebas en taurus ya que creo que es una herramienta que hemos usado con anterioridad en el blog. Vamos a crear un script sencillo para generar una carga durante cinco minutos de diez peticiones por segundo con cinco usuarios concurrentes que aumentaran cada minuto. Podéis verlo en el directorio de purchase-service src/test/resources/taurus-tests

Tras lanzar las pruebas vemos que las peticiones han ido correctamente, sin ningún error y con un tiempo de respuesta estable, tiene sentido debido a que no hemos generado mucha carga.

Purchase load test

Claro… esto como hemos comentado en la introducción, nos da el reporte de las llamadas a purchase-service, pero no podemos diferenciar que parte del tiempo de respuesta corresponde a que las llamadas a las APIs internas, o en caso de que hubiera un error en una API interna cual sería etc. Perdemos cierta visibilidad, y aquí es donde entra el rastreo de las peticiones, que nos dará mucha más información.

En las siguientes imágenes vemos dos peticiones a la API de purchase:

  • La primera se ha realizado al comienzo de la carga, cuando la carga era un usuario y una petición por segundo.
  • La segunda se ha realizado al final de la carga, cuando la carga eran cinco usuarios concurrentes y diez peticiones por segundo.

1s trace

9s traceMediante zipkin vemos de forma desglosada cuanto tiempo se lleva cada API, por lo que en caso de que el tiempo de respuesta sería muy alto o estaría fuera de los límites que hayamos previsto, podríamos ver si el problema está en alguna API interna. O en el caso de que haya algún error en la llamada podríamos tener más información.

Conclusiones

Instrumentar nuestros servicios webs y así poder rastrear las peticiones nos aporta información de gran interés. Ya no solo para controlar el estado de cada petición y respuesta, si no, que puede ser una gran herramienta para realizar pruebas de rendimiento si nuestro sistema se compone de servicios externos e internos.

En el caso de nuestro proyecto nos hemos ahorrado el realizar pruebas de rendimiento para dos servicios internos de forma independiente, pero en el caso de un sistema complejo basado en microservicios, el valor puede ser mucho mayor, ya que las pruebas son más realistas, nos ahorramos pruebas, tiempo etc.

Consumer-driven contract testing con Spring Cloud Contract

Introducción

Como hemos comentado en entradas anteriores, la arquitectura en microservicos cada vez está ganando más fuerza, y esto supone un reto tanto para el desarrollo como para el testing. En el post vamos a hablar sobre Consumer-driven contract (CDC) testing y cómo podemos verificar que los microservicios trabajan correctamente y según lo esperado entre ellos.

Para ello, como siempre vamos a trabajar con un proyecto de ejemplo usando Spring Cloud Contract, que podréis descargar del repositorio de GitHub.

Consumer-driven contract (CDC) testing

Conociendo la arquitectura en microservicios sabemos que el mayor reto consiste en comprobar y validar la comunicación entre los distintos servicios y para ello las estrategias más comunes son ejecutar pruebas end-to-end o crear mocks de los servicios y lanzar pruebas de integración sobre ellos.

MicroservicesCon las pruebas end-to-end vamos a comprobar la comunicación entre los servicios simulando una experiencia más cercana a el entorno de producción. Primero debemos tener levantado todo el entorno (servicios, BBDD) lo que es costoso y dificulta la trazabilidad. Por otro lado, es fácil que los problemas de comunicación hagan que nuestras pruebas se ralenticen más de lo normal o que fallen. Las dependencia de tantos componentes debilitan nuestras pruebas.

Como hemos visto en un post anterior, mockear un servicio nos elimina los problemas anteriores. Por el contrario, puede que las pruebas contra el mock sean satisfactorias, pero en el entorno de producción la aplicación falle debido a un mal diseño de los mocks.

 

 

cdc testingCon Spring Cloud Contract diseñaremos los contratos de los servicios. El contracto sencillamente es la definición del comportamiento de nuestro servicio en base a una request. Con esa definición se van a generar teses a nivel de proyecto para validar los contratos, así como, se publicará el Stub en nuestro repositorio maven contra el que lanzaremos las pruebas de los clientes del servicio (como vemos en la imagen). No obstante, lo vamos a ver ahora y quedará mucho más claro.

Entre los beneficios que hemos visto, utilizar CDC testing con Spring Cloud Contract nos aporta:

  • Poder desarrollar nuestro servicio mediante metodología ATDD.
  • Una documentación actualizada y “honesta” de las APIs que conforman nuestro sistema.
  • Disponer de un mayor control de los clientes que consumen nuestros servicios. Debido a que sabremos quien realiza las pruebas contra los Stubs publicados.
  • Los contratos publicados siempre actualizados, debido que en el proceso de publicación se encuentra integrado en el proceso de “build”.

Entendiendo el proyecto de pruebas

 

project_services

El proyecto con el que vamos a trabajar consiste en un sistema de dos servicios. Inventory-service y Account-service.

Account-service expone una API que devuelve los datos de los usuarios e inventory-service expone una API que devuelve el inventario de un usuario vendedor.

Inventory-service consume account-service para obtener los datos del usuario de los items que va a devolver cuando le llamen, por lo que vamos a lanzar las pruebas sobre el Stub publicado de account-service.

Inventory-service publica su Stub para que cualquier cliente futuro pueda ejecutar las pruebas contra él. Así que vuestra imaginación puede volar y ayudaros a construir un nuevo servicio siguiendo los pasos del CDC 😛

 

Preparando el proyecto

Antes de ponernos a definir los primeros contratos vamos a ver que dependencias necesitamos en nuestro proyecto. Los ejemplos los vamos a ver del proyecto de account-service.

Dentro del build.gradle vamos a definir también en que directorio vamos a alojar los contratos, así como donde se guardarán las clases base de tests que utilizará para generar los teses de verificación de los contratos.

Definiendo los contratos

Siguiendo con account-service vamos a ver el contracto que hemos definido para devolver los datos de un usuario en base a su id, en /src/test/resources/contracts/ShouldReturnAccountById.groovy

Los contratos tienen una semantica muy clara y diferenciada. Vemos como definimos tanto la request que esperamos como la response que devolvemos para esa request.

Con este contracto estamos definiendo que para cuando llamen al endpoint: /account/[0-9]+ vamos a responder con un estado 200 y con un json que contien esos campos y datos.

Vemos que la url tiene un regex, por lo que un ejemplo de una llamada que la cumpla sería: /account/12345.

Es un contrato sencillo, y con esto tanto desarrollador (para comenzar el desarrollo) como el cliente (para consumirlo) ya sabrían cual sería el endpoint, así como los datos que tienen que mandar y los que van a recibir.

Podéis ver toda la información del DSL de los contratos en el siguiente enlace.

Si seguimos la metodología ATDD, sabemos que deberíamos hacer un desarrollo mínimo para poder lanzar los teses y que fallen e ir refactorizando hasta que los teses no fallen.

Veamos el paso de generar los contratos y publicar el Stub. Lanzando el comando generateContractTests va a autogenerar los teses de validación del contracto que hemos hablado anteriormente.


➜ account-service git:(master) ./gradlew generateContractTests
Starting a Gradle Daemon (subsequent builds will be faster)
:copyContracts
:generateContractTests

Y cuando lanzamos el comando install publicará en nuestro maven local (.m2) el Stub. Veámoslo:


➜ account-service git:(master) ./gradlew clean install
:clean
:compileJava
:compileGroovy NO-SOURCE
:processResources NO-SOURCE
:classes
:jar
:copyContracts
:generateClientStubs
:verifierStubsJar
:install

Si accedéis a .m2/repository/com/qajungle/account-service/0.0.1-SNAPSHOT podréis ver el Stub:

account-service-0.0.1-SNAPSHOT-stubs.jar

Desarrollando y ejecutando los teses

El cliente (inventory-service) consume account-service para obtener los datos del usuario del inventario. Veamos la clase de servicio y el gateway sobre el que tendremos que desarrollar el test:

/src/main/java/com/qajungle/services/InventoryService.java

/src/main/java/com/qajungle/gateway/AccountGateway.java

Como vemos AccountGateway.java es el encargado de realizar la llamada al API de account-service. Por lo que en este punto es donde tendremos que atacar contra el Stub publicado por el servicio. Veamos el test:

/src/test/java/com/qajungle/services/InventoryServiceTest.java

Lo más importante en este punto es ver que estamos atacando al Stub del servicio mediante el tag:


@AutoConfigureStubRunner(ids = "com.qajungle:account-service:+:stubs:8082", workOffline = true)

Si lanzamos el test del proyecto veremos que todo ha ido correctamente:


➜ inventory-service git:(master) ./gradlew -Dtest.single=InventoryService test
:compileJava
:compileGroovy NO-SOURCE
:processResources
:classes
:copyContracts
:generateContractTests
:compileTestJava
:compileTestGroovy
:processTestResources
:testClasses
:test

BUILD SUCCESSFUL

Cambiemos el contracto en account-service para comprobar que lanza contra el Stub. Por ejemplo cambiendo el valor de un campo.


➜ inventory-service git:(master) ✗ ./gradlew -Dtest.single=InventoryService test
:compileJava UP-TO-DATE
:compileGroovy NO-SOURCE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:copyContracts
:generateContractTests
:compileTestJava UP-TO-DATE
:compileTestGroovy
:processTestResources UP-TO-DATE
:testClasses
:test


com.qajungle.services.InventoryServiceTest > should_return_seller_information FAILED
org.junit.ComparisonFailure at InventoryServiceTest.java:28

org.junit.ComparisonFailure: expected:<“[aritz]”> but was:<“[fail]”>

El resultado es que el test fallará con un ComparisonFailure en este caso.

Conclusiones

El hacer uso de contract testing nos proporciona unos beneficios que no podemos encontrar en otras estrategias para el testing de servicios. Es muy importante sacar los valores que nos aporta ya no solo en no depender del entorno, de los teses E2E, si no de poder desarrollar mediante ATDD, disponer de una documentación fidedigna y llevar un mayor control de quien consume nuestros servicios.

A día de hoy solo he probado Spring Cloud Contract, pero me consta que existen otras herramientas como Pact, que espero probar en un futuro cercano. No obstante como habéis podido comprobar Spring Cloud Contract no es complejo de utilizar.

Charlando de testing con: Federico Toledo

Introducción

Si hay algo que me gusta y de lo que creo que aprendo es de hablar con los compañeros sobre el mundo del desarrollo y el testing. La mayor parte del conocimiento que tengo hoy en día es gracias a leer artículos y a discutir ideas.

Como creo que es mejor compartir, he decidido crear una nueva serie en el blog en la que colgaré “entrevistas” o “momentos de charla” sobre testing con otros compañeros. Mi idea es hablar sobre testing no solo con testers o QEs, si no con todos los roles implicados en el mundo del desarrollo. Conocer diferentes opiniones y puntos de vista que puedan hacer discutir y replantearme las cosas.

En el primer post de la serie “Charlando de testing con”  lo he decidido hacer con Federico Toledo, tras hablar varías veces con el y leer su libro creo que tiene una opinión muy interesante sobre el actual estado del testing, así como grandes ideas. No me gustaría adelantaros nada así que os presento la charla con Federico Toledo:

Entrevista

 

QA Jungle – Charlando de testing con Federico Toledo from QA Jungle on Vimeo.

Comentarios del libro: Introducción a las Pruebas de Sistemas de Información

Introducción

tapa libro
Hace unos días Federico Toledo escribió un nuevo post en su blog hablando sobre el libro que escribió en el 2014  “Introducción a las Pruebas de Sistemas de Información“. Os recomiendo que os descarguéis el libro gratuito en el mismo post.

En la siguiente lectura voy a tratar de comentar y hacer crítica de lo que me gusta y lo que me gustaría encontrar en el libro. Del porqué recomiendo el libro y en mi opinión a qué tipo de lector o profesional está orientado.

¡Vamos allá!

 

 

El autor

Para mí es muy importante saber quién es el autor de un libro, sobre todo si es técnico. La razón es que muchos libros técnicos comparten el mismo contenido, hablan de los mismos temas y tratan los mismos conocimientos.

¿Entonces donde se encuentra la diferencia? En el autor. El autor aporta su experiencia, sus conocimientos y su personalidad. Eso es lo que hay que buscar en los libros técnicos. Tenemos que mirar más allá de la técnica pura y dura, tenemos que analizar la experiencia del autor sobre el tema que trata. Si no, solo se quedará en un simple manual.

A Federico Toledo le conozco de hablar personalmente alguna vez con él, de leer sus artículos y seguir sus consejos. Tiene un CV y una experiencia envidiable, pero sin entrar a lo personal, es una persona que se centra mucho en la práctica, al menos en los artículos que he leído. Por lo que antes de comenzar a leer el libro me hacía una idea de cómo se iba a enfocar.

Enfoque del libro

Uno de los mayores problemas de los libros técnicos, es la densidad a la hora de leerlos. ¡La mayoría se hacen eternos!

En mi caso ocurre por varios motivos: El autor es muy monoto, el libro no aporta mayor información que otros, no encuentro en el libro lo prometido. Introducción a las Pruebas de Sistemas de Información no ha sido el caso.

He leído varios libros y artículos de testing de diferentes autores y muchos de ellos solo han sido copias de otros, algunos ni siquiera aportaban la experiencia propia. Con lo que al final pensaba que solo aportaban humo.

En el caso de Introducción a las Pruebas de Sistemas de Informacióha sido diferente. Tenía ganas de encontrar un libro que se centrará en la práctica, que me aportara técnicas y estrategias para mejorar mis skills de testing. Bien es cierto que no profundiza demasiado en algunos de los conceptos tratados, tampoco creo que sea ese su objetivo, ya que es un libro de introducción al mundo del testing.

Temas del libro

¿El libro cubre temas suficientemente atractivos para mí? La respuesta es sí.

Cuando leemos libros de testing, nos encontramos con muchos temas sobre cómo implementar el testing en la metodología X. Al final nos encontramos que se han tratado más temas de metodologías y gestión de equipos que de estrategias o técnicas de testing.

Introducción a las Pruebas de Sistemas de Información, en mi opinión toca los temas necesarios para entender y comenzar a aplicar las estrategias y técnicas de testing. Creo que el autor ha buscado ser claro y practico con los temas, y creo que lo ha conseguido.

El que haya una temática tan clara ayuda a que también sea un libro de consulta, me explico. Muchas veces lees un libro técnico y tocas mucha información en varios temas, como si de una novela técnica se tratase. En el caso de Introducción a las Pruebas de Sistemas de Información si necesitas información sobre pruebas de rendimiento, basta con el ir al tema “Pruebas de Performance” para encontrarte toda la información necesaria, sin necesidad de bucear por todo el libro.

Lo que me gusta y lo que me gustaría encontrar


LO QUE ME GUSTA

  • Es un libro muy práctico, aportando ejercicios, lo que ayuda a entender y a interiorizar los temas que se tratan.
  • Para apoyar los ejercicios y las prácticas aporta herramientas al lector. Si de verdad te interesa, no te vas a quedar con la miel en los labios, puedes practicas e incluso utilizarlas para tu día a día en el trabajo.
  • El autor expone casos cotidianos (fuera de toda técnica) y humanos para entender los ejemplos de testing. Esto es muy positivo sobre todo para las personas que comienzan o quieren entender en el mundo del testing.
  • Repasa muchas técnicas de testing y la opinión personal respecto a cada una, con lo que aparte de aportar conocimientos técnicos, también aporta personales. ¡Lo que yo busco en un libro!
  • Aporta diferentes artículos y opiniones respecto a varios temas que trata. Creo que el autor buscar que el lector saque su propia conclusión a través de varios puntos de vista, algo muy positivo ya que ayuda a que el lector pienso por si mismo.

LO QUE ME GUSTARÍA ENCONTRAR

  • Cuando el autor habla de las técnicas de testing, echo en falta tratar la teoría de la pirámide del testing . Durante el tiempo que llevo trabajando como ingeniero de calidad me he encontrado con pirámides invertidas, lo que es signo de no aplicar y desarrollar correctamente el testing. Echo en falta que se trate este tema, ya que creo que es necesario conocerlo y entenderlo para no acabar con suites inmanejables y poco eficientes. No obstante, Federico Toledo si lo ha tratado en la versión en inglés del libro :).
  • Una vez más echo de menos que se traten temas de estrategias y técnicas de prevención. Es cierto que hoy en día el mundo del testing y el QE está orientado a garantizar la calidad mediante diferentes técnicas de pruebas (funcionales, performance, automáticas, unitarias…). Pero creo que sería interesante concienciar en acto de la prevención.
    Estás mismas técnicas pueden ser usadas para prevenir la inserción de fallos y la regresión de calidad, más que buscarlos. Poco a poco se está invirtiendo más tiempo en investigar técnicas de prevención de bugs mediante algoritmos de IA etc.
    Lo más seguro que no sea el “scope” del libro, pero suelo echar en falta algún tema o comentario al respecto, que muestre que el mundo del QE intenta expandirse y evolucionar también hacia otros aspectos.

Conclusión

Tanto si quieres comenzar o entender en el mundo del testing, como si eres un experto y quieres afianzar tus conocimientos te recomiendo la lectura del libro.

En el primer caso vas a ganar los conocimientos suficientes para entender la importancia del testing y comenzar a desarrollar tus habilidades.

En el segundo caso puede que te encuentres que el autor no profundiza, pero hay que entender que no es el “scope” del libro. A pesar de ello vas a poder descubrir y repasar técnicas muy interesantes, así como ver otros puntos de vista que seguramente van a hacer que te lleves una grata sorpresa.

Rest API performance testing con Taurus

Introducción

En el post vamos a tratar paso a paso como comenzar a lanzar pruebas de rendimiento sobre Rest API con Taurus, ejecutar una prueba y analizar los resultados. Por si queréis realizar las pruebas vosotros también os proporciono el código que podréis descargar de GitHub.

Durante los años que he trabajado con pruebas de rendimiento he podido probar varias herramientas de pago y libres. Desde hace unos pocos meses suelo hablar con Federico Toledo, sobre el mundo del testing en general, pero sobre todo le suelo hacer consultas sobre pruebas de rendimiento.

Entre varias recomendaciones, salió Taurus. Taurus es una herramienta open source que nos permite automatizar de forma sencilla nuestras pruebas. Lo interesante de Taurus es que las pruebas se definen mediante YAML, lo que hace que los teses sean más legibles y fáciles de escribir.

Otra característica interesante de Taurus es que nos permite ejecutar pruebas de otras herramientas como JMeter, Gatling, Selenium, JUnit etc. Podéis encontrar la lista en el siguiente enlace.

Entorno de pruebas

Vamos a levantar un servidor Rest API sobre el que vamos a lanzar las pruebas de rendimiento, Json-server.

Lo ideal sería tener una máquina con el servidor y otra con la ejecución de la prueba, si esto no os es posible, o si quereís hacer todo más rápido con fines de aprender, está bien instalar todo junto. No perdamos de vista que al momento de ejecutar pruebas de verdad esto lo debeis separar, para que el sistema y la herramienta de generación de carga no compitan por recursos.

Por lo que si queréis experimentar conmigo, podéis ver como instalarlo en el enlace anterior. 🙂

Objetivo de las pruebas

  1. Aplicación
    Vamos a imaginarnos que tenemos una aplicación en la que almacenamos datos de los posts publicados. Disponemos de una API Rest para que los consumidores puedan listar los posts o buscar un posts concreto. Por otro lado internamente usamos la API para publicar nueva información.
  2. Carga estimada
    Tras un análisis inicial para saber el número de peticiones por minuto (througput) que esperamos en nuestra API se estima que en el peor de los casos puede ser de:
    – 1200 rpm (peticiones por minuto) y una concurrencia de 20 usuarios en el caso del listado de posts (GET: /posts)
    – 1 rpm y una concurrencia de 1 usuario en el caso de la publicación de posts (POST: /posts)
    Lo ideal sería que el tiempo de respuesta fuera inferior a 40 ms.
  3. Que probar
    Tras el análisis inicial debemos saber cual es nuestro objetivo real de las pruebas. Podemos realizar pruebas de rendimiento para ver el porqué la aplicación no responde en un tiempo aceptable, realizar pruebas periódicas para ver que el tiempo de respuesta no se degrada, hasta pruebas para ver el número de máquinas que hacen falta para soportar una carga esperada.
    En este caso vamos a comprobar que para la carga esperada nuestra aplicación responde en un tiempo de respuesta aceptable y sin errores, es decir load testing o pruebas de carga.

Definiendo los casos de prueba

Vamos a configurar la prueba con dos escenarios diferentes:

1.- Escenario de peticiones al listado de posts. 20 usuarios concurrentes con un throughput de 20 rps (en Taurus el throughput se define por segundos), de tal forma que 20 * 60 = 1200 rpm.

2.- Escenario de peticiones al listado de posts. 1 usuario concurrente con un throughput de 1 rps.

En ambos casos para poder ver si el tiempo de respuesta se degrada con el tiempo vamos a ejecutar la prueba durante 1 hora, con una rampa de usuarios de 5 minutos, es decir, cada minuto se añadirán 4 usuarios, de tal forma que cuando pasen los 5 minutos tendremos la concurrencia de 20 usuarios.

La configuración de la prueba es bastante legible, no obstante os dejo el siguiente enlace donde podéis ver la sintaxis de configuración.

Ejecutando las pruebas

En la configuración de las pruebas de carga los dos escenarios se encuentran en la misma configuración “load-api-testing.yml”. Vamos a ver como ejecutarlos.

Para ejecutar las pruebas de carga tenemos que ejecutar el siguiente comando dentro del directorio de las pruebas:

bzt load-api-testing.yml 

taurus execution

Taurus nos proporciona un visor por consola aportando la información básica del estado de la prueba en vivo.

El panel izquierdo de la imagen nos muestra las peticiones que se hacen sobre el servidor api rest que hemos levantado para realizar las pruebas.

Taurus mediante su configuración nos permite subir los resultados al visor de resultados de nuestra cuenta de Blazemeter. Os dejo el siguiente enlace para que veáis como se configura.

 

Para conseguir unos datos objetivos sería deseable ejecutar cada prueba un mínimo de tres veces, siempre y cuando dispongamos de tiempo para poder realizarlas. Esto es normal ya que en una única ejecución hemos podido encontrarnos con problemas externos que pueden desprestigiar los resultados.

Analizando los resultados

Como hemos comentado anteriormente Taurus nos permite subir los resultados al visor de nuestra cuenta de Blazemeter. Vamos a analizar los resultados de nuestra prueba centrandonos en los datos más importantes para el objetivo del post.

Reporte temporal

grafico resultados

A simple vista se ve que el tiempo de respuesta va aumentando con el tiempo, superando incluso el límite que habíamos definido como ideal, RT < 40ms. Esto puede ser debido a dos motivos:

  1. Con el tiempo el sistema se degrada debido a la concurrencia de 50 usuarios.
  2. Debido que metemos un nuevo dato cada segundo, la base de datos crece,  y por lo tanto el tiempo de respuesta aumenta.

Para descartar la primera opción se debería hacer una prueba sin añadir datos cada segundo a la base de datos, de esa forma veríamos si el tiempo de respuesta se mantiene en un rango aceptable.

No obstante si ese fuera el caso, a futuro ibamos a tener el mismo problema ya que en algún momento ibamos a tener ese número de datos en la base de datos, y llevaría a picos en el tiempo de respuesta, como podemos ver entre las 17:05 y 17:15.

Estado de las peticiones

grafico resultados

Hemos visto el gráfico de datos, esto nos ayuda a ver la estabilidad del entorno en las pruebas, ahora vamos a analizar los datos del tiempo de respuesta.

En muchas ocasiones valoramos los datos por la media (avg), en este caso la media del tiempo de respuesta (27ms). Si únicamente nos fijaramos en la media nuestra prueba sería exitosa, y es ese el caso?

En la pestaña de Request Stats nos dan los resultados de los percentiles. Los percentiles nos muestran una medida bajo la cual se encuentra un porcentaje de la muestra, es decir, en nuestro caso el p95 (95%) de la muestra se encuentra por debajo de 73 ms.

Como vemos los resultados no son lo que esperábamos, si lo que deseamos es que la mayoría de lo usuarios tengan un tiempo de respuesta menor que 40 ms. Dista bastante de la media de 27ms.

Os dejo un enlace muy interesante sobre las métricas para performance testing:

https://www.federico-toledo.com/promedio-desviacion-estandar-y-percentiles-metricas-para-testing-de-performance/

Errores

errores

Si volvemos a ver la gráfica del reporte temporal podemos ver que sobre las 16:45, cuando el entorno comienza a desestabilizarse, empezamos a tener picos de errores.

Si vamos a la pestaña de errores podemos ver el número de errores. En el caso de nuestras pruebas la mayoría de errores vienen debido a problemas de conexión o respuesta con el servidor, muestra de que no tolera la carga.

Conclusiones

A pesar de que disponemos de una gran cantidad de herramientas, Taurus nos permite definir las pruebas de forma muy sencilla y reutilizable. Nos aporta los datos necesarios para obtener unos resultados concretos.

No lo he comentado anteriormente pero Taurus se puede integrar de forma muy fácil a nuestro CI, os dejo el siguiente enlace.

Por otro lado, teniendo claro el objetivo de nuestras pruebas y con un buen análisis de los resultados podemos llegar a conclusiones acertadas.

Mutation testing – PIT nuestro gran amigo

Introducción

No siempre las pruebas unitarias que realizamos dan la cobertura suficiente a nuestro código, no nos engañemos. Allá por los años 70-80 se creo un nuevo concepto de testing denominado mutation testing. Consistía en modificar ciertas lineas de nuestro código, para posteriormente probar si en realidad fallaba o no.

futurama_mutantesEstos cambios en el código se denominan mutantesy como si de un juego se tratará, debemos matarlos, es decir, si los teses fallan, se dice que han matado a los mutantes. Cuantos más mutantes matemos, mejor.

En este post vamos a ver como se realiza mutation testing mediante PIT, una herramienta que se va a encargar de realizar las mutaciones y ejecutar nuestros teses unitarios para ver si logra matar todos los mutantes.

Para ello, he dejado en mi github el proyecto con el que he realizado las pruebas. Podéis descargarlo y jugar a matar mutantes 😉

El código es “refactorizable”, lo sé, pero lo que he intentado es disponer de código sencillo que nos permita jugar con las pruebas unitarias y las mutaciones, lo vamos a ver.

Mutación del código

Podéis ver todos los tipos de mutaciones que realiza PIT en el siguiente enlace, pero vamos a ver una mutación sencilla, para ayudarnos a entender mejor lo que son las mutaciones en código.

La mutación de limites condicionales, consiste en cambiar los operadores relacionales de nuestras sentencias. Así comprobaremos que nuestras condiciones están bien construidas. Lo vemos en la siguiente tabla:

Original conditional Mutated conditional
< <=
<= <
> >=
>= >

En nuestro código tenemos el siguiente condicional:

De forma que la mutación condicional que se hará será:

Cuando ejecutemos las pruebas de mutación, PIT se encargará de realizar todas las mutaciones. Por defecto se aplican las mutaciones básicas, pero si queremos ser más especificos o llegar un mayor nivel de mutación, PIT nos ofrece una lista de mutaciones que tendremos que activar por configuración.

Lanzando las pruebas de mutación

A pesar de que las pruebas de mutación nos ayudan a detectar errores en nuestro código, también son muy útiles para comprobar si nuestras pruebas unitarias son correctas o garantizan la cobertura necesaría, por lo que son un gran apoyo.

Veamos la lógica de nuestro código y las pruebas unitarias que garantizan que el código funcione correctamente. Para el producto que queremos comprar, se comprueba que haya la cantidad disponible y en caso correcto se decrementa en 1 el valor de la cantidad y se devuelve. Sencillo.

Nuestras pruebas unitarias comprobaran que se descuentan correctamente los productos y que no podemos comprar más productos de los que dispone la máquina.

Si ejecutamos la pruebas unitarias, nos darán un resultado verde, es decir, todo funcional correctamente! (Según nuestras pruebas)

Unit testing result

Ahora es el turno de lanzar nuestras pruebas de mutación. Para ello, nos situamos en la raiz del proyecto y ejecutamos:
➜ mutation-testing git:(master) ./gradlew pitest


pit coverage

Las pruebas generan un reporte que se almacena en {project}/build/reports/pitest/. Vamos a analizarlos.

En nuestro proyecto de pruebas tenemos dos clases en src/java. En este caso la clase ProductCode es un enum, por lo que vamos a necesitar cobertura de test.

La lógica se encuentra en la clase VendingMachine.java, que es la encargada de gestionar la tienda.

 

En nuestras pruebas unitarias pensabamos que podríamos cubrir todos los casos, pero PIT nos muestra que no es así. Vamos a ver la razón. Las lineas verdes indican que PIT ha matado a los mutantes, mientras que las lineas rojas nos indican que los mutantes han sobrevivido.
pit coverage

Al fijarnos en la lista de mutaciones, vemos que en las líneas 20 y 27 se han realizado dos mutaciones de límite condicional que no han pasado las pruebas.
pit coverage

Si nos fijamos en el código de nuestras pruebas unitarías, vemos que en el test buy_correct_quantity_of_products, no comprobamos en todos los casos que ocurriría sí:

this.pXQuantity = quantity

Vamos a añadir los casos al test a ver que nos dice PIT:

Con los cambios realizados, hemos conseguido matar a todos los mutantes, por lo que los teses han pasado.

new pit result

Conclusiones

Las pruebas unitarías tienen una gran importancía en nuestros desarrollos, si las apoyamos con pruebas de mutación conseguimos garantizar una mayor cobertura.

Con las pruebas de mutación no solo nos curamos en salud de que nuestro código funcione correctamente, si no que comprobamos que nuestras pruebas den la cobertura que esperamos.

Se escucha poco hablar sobre el “mutation testing”, pero bajo mi opinión es un buena práctica el meterlo en nuestra estrategía de calidad, ya que es un arma más para mejorar el alcance de nuestras pruebas.

Bibliografía

  1. http://pitest.org/
  2. https://www.computer.org/csdl/mags/co/1978/04/01646911.pdf
  3. http://antares.sip.ucm.es/tarot09/index_files/MutationTestingTAROT09.pdf