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.”
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.
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.
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!).
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.
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.
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.
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.
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.
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.