Introducción al Contract Testing

En este artículo no sólo vas a poder encontrar una introducción al Contract Testing, descubriendo qué son, sus principales características y qué ventajas tiene su implantación, sino que además vamos a repasar conceptos básicos necesarios para llegar a ese punto.

Vamos a definir qué son unos test de calidad, qué son los test unitarios, qué son los test de integración y qué problemas tienen, y cómo el Contract Testing llega al rescate.

Además mencionaremos unos cuantos recursos interesantes para que puedas entrar en más detalle si lo deseas.

Características de unos tests de calidad

Vamos a empezar el artículo por algo que considero crucial conocer. ¿Qué son unos test de calidad? ¿Qué características deben cumplir? Es mucho más sencillo de enumerar que de llevar a cabo.

Las claves de unos test de calidad son las siguientes:

  • Rápidos.
  • Independientes.
  • Fáciles de mantener.

Test unitarios

Antes de entrar en harina con el Contract Testing, necesitamos tener el concepto de test unitario claro.

Qué son los test unitarios

Un test unitario es una forma de probar de manera aislada una unidad, que es la pieza de código más pequeña que se puede identificar en un sistema.

Normalmente estas unidades son funciones, métodos o propiedades en el código.

La parte de la definición donde se dice de manera aislada cobra mucha importancia en este tipo de test. No se consideran pruebas unitarias si se dependen de sistemas externos (comunicación con la base de datos, comunicación vía red, manipulación del sistema de archivos … ).

¿Cómo podemos hacer entonces en esos casos para probar de forma independiente?

Mocks

La respuesta a la pregunta anterior es apoyándonos en los denominados ‘mock’, que no son más que simulaciones de lo que no queremos probar.

Imagina que tenemos una pieza de código en nuestro sistema que se comunica con un sistema externo de pagos para realizar acciones con su respuesta.

Si somos capaces de simular la el sistema externo de pagos, podríamos probar de forma independiente y unitaria nuestra pieza de código.

De la misma forma en la imagen de abajo puedes ver que podríamos seguir ese mismo método para todas las comunicaciones con servicios externos.

Test unitarios - Mocks

Por qué los test unitarios no garantizan obtener el funcionamiento esperado

Garantizar que cada uno de esos componentes funcionan como se espera de forma independiente, no significa que una vez se integren entre ellos, el resultado sea el esperado.

El problema de las simulaciones es que a menudo traen consigo presunciones que no son ciertas. En el ejemplo anterior, podemos simular la plataforma de pagos con ciertas presunciones, que luego realmente no son ciertas.

Eso puede llevar a que no se represente el comportamiento real en producción.

Debido a que son test unitarios, no estamos probando que esa presunción no es cierta, y eso en definitiva lleva a errores cuando el comportamiento real en producción llega.

Test unitarios no garantizan una integración correcta

Test de Integración

Otro concepto básico que deberíamos tener controlado antes de lanzarnos a las introducción del Contract Testing es el de test de integración. ¡Vamos a ello!

Qué son los test de integración

Los test de integración son aquellos donde se valida que que todas las piezas de un sistema funcionan juntas como se espera.

En la actualidad, cada vez es más difícil encontrar sistemas monolitos, y la mayor parte de los sistemas modernos son sistemas distribuidos, que se conforman de distintas pequeñas piezas que se comunican entre ellas.

Existen muchos tipos de test de integración, pero los más conocidos son los llamados test de integración end-to-end, que necesitan de todos los componentes del sistema levantados en un entorno real (cuanto más parecido al de producción mejor), y sobre ese entorno se lanzará la batería de pruebas.

Test de integración en un sistema de microservicios

Problemas con los test de integración

  • Lentos.
    • Probar sobre sistemas reales, provoca acciones reales, que normalmente, no son eficientes.
    • Normalmente son procesos que no pueden ejecutarse de forma paralela.
  • Frágiles.
    • Habitualmente pueden ser test no deterministas. Por si necesitamos refrescar qué significa esto, es que al ejecutar varias veces los mismos pasos, no siempre se va a obtener el mismo resultado.
    • Se necesita que todos los servicios del sistema real estén levantados y si hay algún cambio en cualquiera de ellos, puede provocar que los test fallen.
    • Debido a la dificultad en orquestar un entorno entero de testing, a menudo se producen falsos positivos, derivados de problemas de configuración no relacionados con cambios de código.
  • Costosos de mantener.
    • Encontrar la causa real de un fallo, puede ser muy costoso. Usando la imagen de arriba de ejemplo, si el fallo viene del microservicio B, puede que eso no sea visible desde la respuesta que da al cliente el A, y por tanto habría que entrar en detalle a ver los logs con un correlation Id para poder identificar la request y las distintas llamadas entre microservicios hasta llegar a la verdadera causa del fallo.
  • Cobertura.
    • Probar todos los escenarios puede llevar demasiado tiempo.
    • Las combinaciones posibles para probar todo el sistema atacando sólo al microservicio A pueden ser inmanejables.
  • Despliegues.
    • Debido a que se prueba todo junto de forma integrada, se necesita desplegar todo junto de la misma manera cuando se vaya a producción. Puede que si sólo se despliega un único componente, el sistema rompa.
    • Alto nivel de acoplamiento y dependencia entre distintos equipos.
  • Escalabilidad.
    • Aumentar el número de equipos y componentes del sistema de forma lineal, causa una curva exponencial en el número de entornos, aumentando por tanto los costes, la complejidad y el tiempo de despliegue.
    • El riesgo asociado a cada cambio sufre también un aumento exponencial.
    • El tiempo de desarrollo se ve bloqueado por las interconexiones y por tanto aumenta la espera en estado ocioso.

Si quieres indagar más sobre los problemas de los test de integración, en este artículo de Pact Flow se expone de forma científica.

Relación entre numero de componentes vs coste & complejidad

Contract Testing

Como ha quedado patente en la sección de test de integración, se tienen bastantes problemas que solucionar y se necesita algo más para ayudar en esta fase y asegurar que el sistema en conjunto se comporta como se espera.

Para eso es para lo que precisamente nace el Contract Testing.

Qué es el Contract Testing

El Contract Testing es una metodología con la que se puede asegurar que dos sistemas separados son compatibles y capaces de comunicarse sin problemas el uno con el otro.

Cuando hablamos de sistemas separados, podemos llamarlo también microservicios por ejemplo. Y cuando mencionamos comunicación entre ellos, podemos dar nombres en esta comunicación, siendo uno el Consumidor y otro el Proveedor.

Consumidor - Proveedor

En el Contract Testing se capturan todas las interacciones entre cada Consumidor y Proveedor quedando almacenadas en un contrato, que a posteriori será utilizado para verificar que ambas partes se adhieren al mismo.

Contract Testing se sitúa en la capa de testing de servicio, ya que son rápidos de ejecutar y no son dependientes de otros sistemas externos.

¿Qué hace diferente al Contract Testing de otros enfoques?

  • Se genera un contrato que viene dado por el código, por lo que siempre representa la realidad.
  • Permite independizar sistemas, probándose de forma independiente.

Funcionamiento del Contract Testing

Funcionamiento del Contract Testing

El Contract Testing comienza estableciendo lo que el consumidor espera recibir como respuesta cuando llame al proveedor con unas determinadas peticiones. Esto se realiza simulando el Proveedor.

Dichas interacciones son almacenadas generando un contrato, que será compartido en toda la infraestructura.

Posteriormente, se repite una a una todas las interacciones que contiene el contrato ejecutando el proveedor y comprobando que éste responde lo esperado ante esas peticiones recibidas.

Los test del Proveedor simularán cualquier otro sistema para permitir ser probados de manera aislada.

Gracias al Contract Testing podremos, de una forma eficaz, garantizar que la integración es correcta y que el sistema en su conjunto funciona como se espera.

Provider-driven Contract Testing

Una API (Proveedor) especifica un contrato, y publica ese documento a todos sus consumidores.

Estos consumidores se adaptan al contrato y utilizan el API para cumplir con sus requisitos.

Problemas con el Provider-driven Contract Testing

El problema con este diseño son los cambios que puede sufrir el contrato de la API. Es fácil que un cambios en el contrato del API resulte en romper el funcionamiento de ciertos consumidores.

A priori no se conoce qué parte de la API es usada por cada consumidor, así que hay que tener un especial cuidado en no realizar cambios que no sean backward compatibles.

Consumer-driven Contract Testing

Invierte el orden de la relación Proveedor-Consumidor, siendo ahora los consumidores los que publican lo que necesitan al proveedor.

Cada Consumidor usa un subset del API. Y por tanto el API sólo necesita implementar el superset de todos los subsets de los contratos que publicaron los Consumidores.

Beneficios del consumer-driven Contract Testing

  • En todo momento se puede conocer cuándo se va a romper un consumidor.
  • Se tiene una documentación actualizada de cada consumidor.
  • Se pueden probar cosas de manera independiente.

Cómo implementar Contract Testing

Herramientas como Pact flow permiten una sencilla implementación de Contract Testing.

¿Qué es Pactflow?

Pact flow es en esencia una versión vitaminada de Pact Broker, con ciertas funciones adicionales para equipos y empresas que busquen implementar Contract Testing.

¿Qué es Pact Broker?

Pact Broker es una aplicación alojada externamente que se ejecuta de forma permanente con una API y una interfaz de usuario que permite intercambiar los contratos y los resultados de verificación que generan las herramientas de Pact. Es también un software de código abierto.

¿Qué es entonces Pact?

Pact es un framework de código abierto que permite implementar Contract Testing utilizando un proveedor y un consumidor simulados.

  • Está basado en Consumer-Driven Contract Testing.
  • Se puede ejecutar tanto en local como en un ciclo de Integración Continua en una máquina en remoto.
  • Tiene soporte para los principales lenguajes (Java, Ruby, .Net, PHP, JS …).
  • Tiene una comunidad activa y su documentación es bastante completa.

Pasos del Contract Testing con Pact flow

El flujo del Contract Testing podría resumirse en dos partes:

  1. Play & Record
  2. Replay & Verify

Desarrollando un poco más estos dos puntos, la primera fase será en la que se ejecutan en el consumidor ciertas interacciones previamente definidas y se graban, y la segunda fase será en la que se repiten estas interacciones esta vez en el proveedor y se verifica que la respuesta es la deseada.


Como ya sabemos, Pact flow se basa en consumer-drive Contract Testing. Es por eso que lo primero será definir qué debe responder el proveedor, ante ciertas peticiones del consumidor. Así quedan definidas las expectativas del consumidor.

Ejemplificando podríamos decir que el consumidor especifica que dada una request GET /product/1234, se espera una response

{

“id”: 1234,

“name”: “pizza”,

“type”: “food”

}
Paso 1 en el Contract Testing

Tendremos que crear las pruebas que representen las interacciones anteriormente definidas.

Ejecutar las pruebas unitarias mediante Pact.

Lo que va a hacer Pact es levantar en la máquina local un servicio para simular el proveedor, ya que nunca se permitirá la comunicación entre ellos, y va a simular las respuestas del API del proveedor, según lo especificado en los test del consumidor.

Paso 2 en el Contract Testing

Este proceso se repetirá por cada necesidad (interacción) que el consumidor tenga del proveedor.

Hasta aquí, si las pruebas del consumidor son satisfactorias, nos permitirá verificar que es compatible con las respuestas del proveedor.

Además, si las pruebas son satisfactorias, Pact generará lo que se llama un contrato Pact, que contendrá cada una de las interacciones realizadas y lo va a compartir con la herramienta Pact Broker, la cual ayudará a compartir el contrato del pacto y la versión del mismo a lo largo de todo el ecosistema Pact.

Por último, entra en juego la parte del Proveedor.

Arrancamos el proveedor y en el lado del consumidor lanzaremos el verificador de pactos indicando la ubicación del fichero del contrato.

Pact va a obtener dicho contrato, y va a simular el consumidor, ejecutando el API del proveedor con cada una de las interacciones que fueron grabadas.

Paso 3 en el Contract Testing

Se comprobará que cada una de las respuestas generadas por el proveedor coinciden con la respuesta que esperaba el consumidor, y de ser así, la prueba se dará como satisfactoria y se tendrá un contrato que asegura que los dos mocks del Consumidor y Proveedor son realmente válidos.


Pact combina la idea de rapidez, de componentes independientes y de test unitarios con contratos para asegurar el correcto funcionamiento del sistema en conjunto.

Objetivos

  • Garantizar que los sistemas con los que el módulo probado se integra son compatibles con el código que se quiere desplegar.
  • Eliminar los test de integración end-to-end.
  • Simplificar los entornos de testing.

Beneficios de Contract Testing

  • Se prueba sólo una funcionalidad a la vez, y de manera independiente. No se necesita desplegar.
  • Se eliminan entornos dedicados para testing.
  • Se obtiene un feedback confiable y de manera rápida. Se van a detectar los fallos en tu máquina y no se va a necesitar andar investigando en ningún sistema de logs.
  • Lost test escalan de forma lineal a medida que el sistema se hace más y más grande.
  • Debido a que se está probando funcionalidad de forma independiente, se puede desplegar módulos independientes de forma segura. Elimina la necesidad de los despliegues de todo el sistema a la vez, con todos los beneficios que ello conlleva.

Casos de uso

  • Aplicaciones web con Javascript / React.
  • Aplicaciones móviles nativas.
  • Microservicios RESTful.
  • Servicios de mensajería asíncrona como Kafka.

Recursos para adentrarse en el Contract Testing

Vamos a empezar por unos cuantos vídeos:

Un par de artículos con ejemplos de código:

Son artículos de 2017, por lo que ciertas partes están algo desactualizadas, pero como concepto sirven para hacerse una idea.

Otro articulo informativo