Contract Testing con Pact - La guía definitiva

Contract Testing con Pact - La guía definitiva

Alejandro Pena, Chapter Lead Backend

Alejandro Pena

Chapter Lead Backend

19 de diciembre de 2024

Este artículo resume qué es el Contract Testing y las diferencias entre las dos principales estrategias de testing. Después de leerlo, tendrás (esperemos) suficiente información para determinar si una de ellas es una buena opción para tu proyecto.

El Contract Testing se centra en verificar las interacciones entre distintos componentes, ya sea en una arquitectura de microservicios (el contexto más habitual) o en cualquier otro tipo de entorno distribuido. A diferencia de las pruebas de integración tradicionales, no comprueba todo el sistema en su conjunto. En su lugar, valida que cada servicio (consumidor y proveedor) se adhiere a un "contrato" mutuamente acordado que define cómo se comunican.

El objetivo principal es garantizar que los cambios en un servicio no rompan la funcionalidad de otro.

Ventajas en el ciclo de desarrollo

El Contract Testing desempeña un papel crucial en la mejora del ciclo de vida del desarrollo de software:

  • Mejora de la agilidad
    Los equipos pueden desarrollar y lanzar servicios de forma independiente sin esperar a la integración completa del sistema. Esta autonomía permite realizar iteraciones más rápidas y reducir los plazos de entrega.

  • Aumenta la confianza en la integración
    Aumenta la confianza en que los cambios en un servicio no afectarán inesperadamente a otros, reduciendo la necesidad de extensas pruebas manuales de integración.

  • Mejora de la calidad global
    "Shifting left" ayuda a detectar errores de integración en una fase temprana del proceso de desarrollo, evitando que los problemas lleguen a entornos posteriores. Este enfoque proactivo conduce a versiones más estables y a una experiencia más fluida para los desarrolladores.

¿Quieres más detalles sobre el impacto que puede tener en tu negocio? Echa un vistazo a nuestro artículo sobre el ROI de Contract Testing.

Tipos de Contract Testing

Consumer Driven

Con esta estrategia, el consumidor dirige la metodología (quién lo iba a esperar, dado el nombre...). El consumidor define las expectativas de la interacción. El contrato especifica las peticiones que el consumidor enviará y las respuestas que espera del proveedor, verificando que es capaz de gestionar las respuestas dadas. Los contratos se generan durante la fase de creación y prueba, en la que el marco de Pact pone en marcha un servidor simulado para validar las interacciones definidas. Si todo funciona como se espera, se genera un archivo de contrato.

Por otro lado, el proveedor es responsable de verificar cada uno de los contratos relacionados con él. El proveedor recupera los contratos relevantes de PactFlow (u otro Pact Broker), y las librerías Pact los utilizan durante la fase de construcción. El proceso implica iniciar un simulacro de consumidor que ejecutará las peticiones definidas contra el código real del proveedor, y verificar que las respuestas son las esperadas.

Puntos clave

  • Los consumidores definen los contratos como "contratos mínimos viables". 

  • El proveedor debe incluir un código relacionado con la verificación en sus clases de prueba para garantizar que se cumplen estos contratos.

Este planteamiento hace recaer en el consumidor la responsabilidad de definir lo que necesita, y el proveedor adapta su aplicación para satisfacer esas expectativas.

Casos prácticos

En escenarios donde tienes control sobre ambos lados de la comunicación, como una arquitectura interna de microservicios, valoras cada componente, sabiendo no solo sobre quién consumen sino también quién los consume.

Buscamos que los componentes funcionen como un equipo, aunque estén gestionados por equipos diferentes.

Bi-Directional

En este caso, el nombre no se explica por sí mismo. En el lado del consumidor, nada cambia; el marco sigue esperando el mismo proceso: definir las expectativas y ejecutar pruebas durante la fase de construcción y pruebas. El contrato se publica en PactFlow (nota: este enfoque no está soportado por el broker de pactos OSS, al menos no todavía).

La principal diferencia radica en el proveedor. Con las pruebas bidireccionales, el proveedor no necesita añadir código de prueba. En su lugar, se espera que el proveedor (o cualquier otro agente en su nombre) publique una Especificación OpenAPI (OAS) a PactFlow. Esta OAS debe ser válida (ya sea generada a partir de código o, si eso no es posible, al menos validada utilizando cualquier herramienta de prueba de su elección). PactFlow confiará en el equipo del proveedor para mantener esta OAS como la fuente de la verdad.

La verificación la realiza el propio PactFlow, comparando el contrato de pacto publicado por el consumidor con la especificación OAS del proveedor.

A partir de este momento, nada cambia. Los flujos de trabajo, la automatización y otros procesos siguen siendo los mismos.

Puntos clave

  • Los consumidores siguen definiendo los contratos como "contratos mínimos viables".

  • El proveedor no necesita implementar código de prueba específico, simplemente necesita tener su OAS en PactFlow.

Casos prácticos

Este enfoque tiene sentido cuando no se tiene (o no se quiere tener) control sobre la base de código del proveedor. Por ejemplo, podría tratarse de una API que se integra con demasiados consumidores como para hacer viables las pruebas personalizadas, tal vez una API heredada que ya no evoluciona, o incluso un componente de terceros. El objetivo es ofrecer una alternativa a las pruebas dirigidas al consumidor en situaciones en las que la participación directa del proveedor es limitada.

Opinión personal

Siempre que sea posible, opte por las pruebas orientadas al consumidor. En mi experiencia, aporta más valor. El conocimiento compartido y la mayor integración que fomenta son inestimables.

Flujo de trabajo

Consumer Driven

ContractTestingconPact1.webp

  • El consumidor define las expectativas en su código base.

  • Durante la fase de construcción y prueba, el marco Pact inicia un proveedor simulado para probar las expectativas utilizando peticiones reales.

  • Si las pruebas se superan con éxito, se genera un archivo Pact (en formato JSON) y se publica en PactFlow o en el pact-broker.

Del lado del proveedor (proceso independiente):

  • El proveedor comienza su proceso de construcción. Descarga todos los contratos relacionados de PactFlow o del intermediario de Pact durante la fase de prueba.

  • Para cada contrato, el marco Pact inicia un servicio simulado de consumo y valida las expectativas utilizando solicitudes reales.

  • Se publica una comprobación de verificación en PactFlow para los contratos que han sido verificados.

Bi-Directional

Contracttesting31.webp

  • El consumidor define las expectativas en su código base.

  • Durante la fase de construcción y prueba, el marco Pact inicia un proveedor simulado para probar las expectativas utilizando peticiones reales.

  • Si las pruebas se superan con éxito, se genera un archivo Pact (en formato JSON) y se publica en PactFlow.

Del lado del proveedor (proceso independiente):

  • El proveedor publica su Especificación OpenAPI (OAS) a PactFlow, ya sea generada a partir de código (preferido) o validada utilizando cualquier herramienta de prueba de su elección (este paso no está cubierto, ya que no es parte de la prueba de contrato en sí).

  • PactFlow verificará la compatibilidad entre los contratos publicados y la OAS.

Codificación

Consumer Driven

En el lado del consumidor, tendrás varios pares como los que se muestran en el siguiente código. @Pact se utiliza para definir las expectativas, @PactTestFor se utiliza para probar las expectativas definidas, y el método @BeforeEach asegura que nuestras pruebas están apuntando al servidor falso iniciado por el framework.

Código del consumidor

@SpringBootTest
@ExtendWith(PactConsumerTestExt.class)
clase StudentProviderTest {


   public static final String STUDENT_1_EXISTS = "el estudiante con ID 1 existe";


   private StudentService studentService;


   @Pacto(consumidor = "consumidor", proveedor = "estudiante-proveedor")
   public V4Pact getStudentWithId1(PactDslWithProvider builder) {
       return builder.given(ESTUDIANTE_1_EXISTE)
               .uponReceiving("obtener un alumno existente")
               .path("/estudiantes/1")
               .method("GET")
               .willRespondWith()
               .status(200)
               .headers(Map.of("Content-Type", "application/json"))
               .body(newJsonBody(objeto -> {
                   object.numberType("id", 1L);
                   object.stringType("nombre", "Nombre falso");
                   object.date("nacimiento", "aaaa-MM-dd", LocalDate.parse("2000-01-01"));
                   object.numberType("créditos", 30);
                   object.stringMatcher("email", Regex.EMAIL, "some.email@sngular.com");
                   object.object("dirección", dirección -> {
                       address.stringType("calle", "123 Main St");
                       address.stringType("ciudad", "CualquierCiudad");
                       address.stringType("zipCode", "12345");
                   });
                   object.minArrayLike("cursosinscritos", 2, curso -> {
                       course.stringType("courseName", "Introducción a la informática");
                       course.stringType("profesor", "Dr. Tech");
                       course.numberType("créditos", 3);
                   });
               }).build())
               .toPact().asV4Pact().get();
   }


   @AntesDeCada
   void setup(MockServer mockServer) {
       RestTemplate restTemplate = new RestTemplateBuilder().rootUri(mockServer.getUrl()).build();
       studentService = new StudentService(restTemplate);
   }


   @Prueba
   @PactTestFor(pactMethod = "getStudentWithId1")
   void getStudentWhenStudentExist() {
       Estudiante esperado = getStudentSample();


       Estudiante estudiante = studentService.getStudent(1L);


       assertDetallesDelEstudiante(esperado, estudiante);
   }
}

Código del proveedor

Mientras tanto, la parte proveedora tendría que añadir pruebas para cubrir todos los estados definidos en sus contratos con consumidores (como "Student 1 exists" en nuestro ejemplo).

@PactBroker
@Provider("estudiante-proveedor")
@SpringBootTest()
clase StudentProviderVerificationTest {


   public static final String STUDENT_1_EXISTS = "el estudiante con ID 1 existe";


   @TestTemplate
   @ExtendWith(PactVerificationInvocationContextProvider.class)
   void verifyPact(PactVerificationContext context) {
      context.verifyInteraction();
   }


   @AntesDeCada
   void setUp(PactVerificationContext context) {
      MockMvcTestTarget      testTarget = new MockMvcTestTarget();
      testTarget.setControllers(studentController);
      testTarget.setControllerAdvices(customExceptionHandler);
      context.setTarget(testTarget);
   }

   @Estado(ESTUDIANTE_1_EXISTE)
   public void alumno1Existe() {
      Estudiante uno = createFakeStudent(1L);
      when(studentRepository.findById(1L)).thenReturn(Optional.of(one));
      when(studentRepository.findAll()).thenReturn(List.of(one));
   }
}

Bi-Directional

El mismo código de ejemplo del consumidor es válido para la opción bidireccional. No hay ningún cambio en la forma de implementar, utilizar, automatizar o desplegar los contratos en el lado del consumidor.

En cuanto al proveedor, no tendrás que añadir código de prueba a tu código base. Recuerda que solo se requiere la especificación OpenAPI.

Idealmente, el mejor enfoque (IMHO) es generarlo directamente desde tu código usando herramientas como el springdoc-openapi-maven-plugin o cualquier otra herramienta de tu elección. También es válido generarlo externamente y validarlo utilizando herramientas de prueba como ReadyAPI, RestAssured, Dredd o Postman. Puedes encontrar un montón de ejemplos y documentación en la web de PactFlow:

Automatización (CI)

Tenemos un par de artículos detallados que cubren los flujos de trabajo y las implicaciones de CI/CD para el enfoque orientado al consumidor, pero también se aplica al bidireccional. Siéntase libre de profundizar en ellos:

Consumer Driven

Puntos clave::

  • Los constructores de consumidores publicarán sus contratos en PactFlow.

  • Los proveedores descargarán los contratos relacionados de PactFlow para validarlos y publicar los resultados.

  • Can-I-Deploy y otros controles de calidad existen tanto para consumidores como para proveedores, como todos sabemos.

Tu trabajo consistirá en orquestar este proceso: gestionar cómo se etiquetan, organizan y filtran para su descarga los contratos publicados/descargados.

Esta estrategia da prioridad al consumidor. Lo ideal es que cada cambio comience en el lado del consumidor. Sin embargo, esto no significa que el proveedor dependa totalmente de las hojas de ruta de los consumidores. Cada parte puede evolucionar de forma independiente, aunque la evolución de los contratos esté impulsada por los consumidores.

Ejemplo de pasos básicos para el consumidor:

ContractTestingconPact7.webp

Para el proveedor, la verificación se realiza normalmente delegando en el plugin de pacto ejecutado dentro de la compilación:

ContractTestingconPact51.webp

Bi-Directional

Puntos clave:

  • Los constructores de consumidores publicarán sus contratos en PactFlow.

  • El proveedor no descarga ningún contrato de PactFlow. Su única responsabilidad es publicar la especificación OpenAPI.

  • Can-I-Deploy y otros controles de calidad existen tanto para consumidores como para proveedores, como todos sabemos.

Como puedes ver, la única y muy importante diferencia es sólo la construcción del proveedor. No descarga los contratos y los valida usando su código y pruebas. El proveedor sólo publica el OAS, y PactFlow hará su magia para comparar ese OAS con el contrato y comprobar la compatibilidad.

Con este enfoque, un ejemplo de creación de un proveedor tendría este aspecto:

ContractTestingconPact5-1.webp

Aplicar simultáneamente ambas técnicas es muy fácil y cómodo.

Desafíos

Infraestructura y requisitos

Aunque la configuración técnica es sencilla (al menos en teoría...), la realidad a menudo implica navegar por complejidades organizativas y técnicas.

Necesitarás configurar PactFlow (o pact broker) como un componente central de tu SDLC. Considéralo tan crítico como tu herramienta CI/CD, ya que se convierte en el centro de gestión y verificación de contratos entre servicios, desempeñando un papel fundamental a la hora de permitir o bloquear despliegues.

Ten mucho cuidado a la hora de diseñar e implementar tus procesos de automatización. Aunque los pasos básicos, como la publicación de contratos, su verificación y la realización de comprobaciones can-i-deploy son esenciales, la verdadera complejidad suele residir en garantizar el etiquetado, el versionado, el filtrado y la organización adecuados de los contratos. Estos aspectos son cruciales para mantener la claridad, escalabilidad y eficiencia a medida que crece el sistema. 

El artículo Contract Testing Workflows es una excelente referencia para este asunto.

Complejidad de la adopción

Uno de los retos más importantes, especialmente en las grandes organizaciones, es impulsar la adopción. Promover esta práctica requiere un esfuerzo continuo de relaciones con los desarrolladores (DevRel), que incluye educar a los equipos, proporcionar apoyo y ofrecer formación para garantizar la alineación entre los departamentos. La transición a un enfoque que dé prioridad a las pruebas por contrato también puede implicar la resistencia de equipos acostumbrados a las pruebas de integración tradicionales o que dudan en invertir tiempo en aprender nuevas metodologías.

En la sección "Incorporación de equipos" de nuestro artículo "Pruebas y desarrollo de contratos" encontrarás algunas ideas útiles.

Sincronización de contratos

La evolución de las versiones de los contratos resulta más sencilla con el tiempo, a medida que los equipos se familiarizan con el marco. Sin embargo, el verdadero reto consiste en gestionar situaciones específicas o evoluciones de servicios que requieren un tratamiento personalizado. La clave es ser estricto con la metodología y permitir al mismo tiempo las personalizaciones necesarias.

Aunque todos conocemos la teoría básica, es inevitable que surjan casos especiales. Echa un vistazo a los ejemplos tratados en el artículo Contract Testing & CI para conocer cómo gestionarlos.

Pact Broker vs PactFlow

¿Es suficiente la versión OSS gratuita (Pact Broker) o es necesario PactFlow? La respuesta, como siempre, es "depende".

Pact Broker (la versión de código abierto) funcionará en muchos contextos. Supondrá una gran mejora para tu conjunto de pruebas y te proporcionará la mayoría de las ventajas de las pruebas de contratos. Sin embargo, no permite realizar pruebas bidireccionales. Si esa característica es crucial para ti, o si necesitas apoyo comercial, entonces la decisión de optar por PactFlow se hace más clara.

A continuación encontrarás un cuadro que resume las principales diferencias entre las dos opciones para ayudarte a tomar una decisión con conocimiento de causa:

Pact Broker PactFlow
Consumer Driven)
Bi-Directional 🟥 🟥
TC aumentada por IA 🟥
Formatos admitidos Pact Pact + OpenAPI
Integración con SwaggerHub 🟥
Hosting Autoalojado SaaS o autoalojado
Funciones, gestión de usuarios, equipos, etc 🟥
Acceso y administración seguros (SAML) 🟥
Interfaz de usuario Básico Avanzado
Ayuda Sólo para la Comunidad
Tokens API 🟥
Secretos 🟥
Registros de auditoría 🟥

Conclusión

En el mundo actual centrado en las API, donde API-first es la norma, el Contract Testing ya no es opcional. Es una práctica fundamental para garantizar la solidez de tus sistemas y el éxito de tu estrategia digital.

Pact, como marco de código abierto, y PactFlow, como herramienta con licencia, son las principales opciones para implantar esta práctica. No importa cuál elija, la adopción de pruebas por contrato es siempre una victoria para tus equipos (¡y para tu negocio!). De nuevo, te animo a que consultes nuestro artículo sobre el ROI para obtener más información).

Gracias por leerme.

Alejandro Pena, Chapter Lead Backend

Alejandro Pena

Chapter Lead Backend

With over 14 years of experience as a Software Engineer, I specialize in Backend, Quality Engineering, and DevOps. My passion for exploring new technologies is matched by my love for video games. I enjoy hiking with my family, watching movies, reading, or listening to music in my free time.


Nuestras últimas novedades

¿Te interesa saber cómo nos adaptamos constantemente a la nueva frontera digital?

Agilidad, complejidad y método empírico
Agilidad, complejidad y método empírico

Insight

18 de diciembre de 2024

Agilidad, complejidad y método empírico

Tecnologías rompedoras hoy, que redibujarán el mapa de la innovación en 2025
Tecnologías rompedoras hoy, que redibujarán el mapa de la innovación en 2025

Insight

10 de diciembre de 2024

Tecnologías rompedoras hoy, que redibujarán el mapa de la innovación en 2025

Desarrollo sostenible: Minimizando la huella digital y optimizando el consumo
Desarrollo sostenible: Minimizando la huella digital y optimizando el consumo

Insight

3 de diciembre de 2024

Desarrollo sostenible: Minimizando la huella digital y optimizando el consumo

El Acta Europea de Accesibilidad (EAA): hacia un presente digital más inclusivo
El Acta Europea de Accesibilidad (EAA): hacia un presente digital más inclusivo

Tech Insight

2 de diciembre de 2024

El Acta Europea de Accesibilidad (EAA): hacia un presente digital más inclusivo