Pruebas de contratos de cara al consumidor (DbC - Design by Contract)

Pruebas de contratos de cara al consumidor (DbC - Design by Contract)

La estructura de microservicios ha sido una tendencia que varias empresas han adaptado a esta práctica en los últimos años. Con los microservicios, los grandes proyectos de software se fragmentan en unidades o elementos más pequeños, desarrollados de forma autónoma por diferentes equipos.

Cada uno de estos elementos distintos tiene una responsabilidad específica y tiene su propia infraestructura para que los cambios puedan probarse e implementarse de forma independiente, lo que muestra numerosas ventajas en comparación con la arquitectura monolítica. Sin embargo, este nuevo enfoque también conlleva nuevos obstáculos para las pruebas.

En este artículo, exploraremos por qué las técnicas de prueba tradicionales pueden no ser las más adecuadas para evaluar microservicios y cómo podemos probarlas de manera eficiente mediante pruebas por contrato.

La adopción de numerosos servicios independientes más pequeños aporta ventajas, pero, por otro lado, plantea desafíos en comparación con los enfoques de prueba convencionales, ya que la verificación de la integración se vuelve compleja a medida que aumentan los puntos de conexión entre diferentes servicios.

Las técnicas de prueba convencionales, como la integración integral, pueden parecer la mejor opción, pero estas pruebas se vuelven problemáticas al evaluar microservicios ya que son lentas, propensas a errores y difíciles de mantener, lo que lleva a un proceso de retroalimentación lento. Además, requiere entornos de extremo a extremo adecuados en los que los servicios se integren entre sí con los datos de prueba necesarios, configuraciones, etc.

Si estamos tratando con sólo dos servicios, esta forma de prueba puede ser adecuada, pero imaginemos cuando hay cientos o miles de servicios diferentes comunicándose, como Amazon o Netflix. ¿Cómo asegurar que todas las modificaciones realizadas no causen problemas en otros servicios? ¿Cómo garantizar la eficiencia en el mantenimiento de los datos de prueba?

Las pruebas de integración de un extremo a otro requieren que los entornos de prueba adecuados estén conectados entre sí, pero implementar este enfoque para microservicios puede terminar generando más desafíos que beneficios. En cambio, si optamos por pruebas aisladas y creamos simulaciones de dependencias externas, las pruebas sin duda serán rápidas y fáciles de mantener.

Sin embargo, ¿qué confianza tenemos en que la simulación de dependencias refleja con precisión el comportamiento real del servicio? ¿Cómo podemos solucionar este problema? Afortunadamente, existe otra capa de pruebas que podemos aplicar para ayudar a evaluar los microservicios de manera eficiente.

Las pruebas de contrato significan que comparamos nuestra API con un conjunto de expectativas (contratos). Esto significa que queremos verificar que al recibir una llamada específica, nuestro servidor proveedor de API devolverá los datos que especificamos en la documentación. A menudo no tenemos información precisa sobre las necesidades de nuestros consumidores de API.

Para superar este problema, los consumidores pueden establecer sus expectativas como simulaciones que utilizan en pruebas unitarias, creando contratos que esperan que cumplamos. Podemos juntar estas simulaciones y verificar que nuestro proveedor devuelva datos iguales o similares cuando se llama de la misma manera que está configurada la simulación, esencialmente probando el límite del servicio. Este enfoque se denomina prueba de contrato impulsada por el consumidor.


La ventaja de las pruebas de contrato es que no hay necesidad de entornos integrados, ya que las expectativas del cliente se registran en este contrato y se validan con un servicio de proveedor simulado. Luego, el contrato se carga en Pact Broker y luego el proveedor ejecuta su prueba y verifica que todo lo escrito en el contrato sea correcto.

Las pruebas de contrato, aunque utilizamos un servicio simulado para validar nuestras pruebas, aún resuelven los problemas de implementar cambios rotos en producción, ya que se notifica a los clientes cuando el proveedor implementa un cambio que causa que el contrato se rompa y viceversa.

Al igual que los contratos o contratos de la vida real, si es necesario realizar un cambio en el contrato, se notificará a ambas partes y los cambios solo se realizarán cuando todos estén en la misma página y lo aprueben.

Arquitectura básica de la prueba de contrato

Architetura de testes de contrato direcionados ao consumidor


Limitaciones de la prueba de contrato

De todos modos, no puede utilizar pruebas de contrato solo cuando pruebe microservicios y no reemplace las comunicaciones reales entre diferentes equipos. Las pruebas de contrato no reemplazan las pruebas funcionales, ya que no probamos el comportamiento de nuestros servicios.

Tampoco es recomendable utilizar pruebas de contrato si tiene una API pública, porque no sabe cuántos clientes tiene y cómo utilizan su servicio.

Terminología abordada

  1. Consumidor - Servicio que consume datos de un proveedor;
  2. Proveedor - Servicio que ofrece datos al consumidor;
  3. Contrato/Pacto - Documento base del contrato entre el consumidor y el proveedor (normalmente en formato JSON que captura qué solicitudes necesita el consumidor del proveedor y qué tipos de datos, códigos de estado y respuestas devolverá el proveedor);
  4. Pact Broker - Servicio alojado que almacena todo el contrato (canal de comunicación entre consumidores y proveedores).


Cómo implementarla

Para implementar la prueba de contacto, utilizaremos la herramienta Pact de contrato de código aberto. En el siguiente enlace puedes ver paso a paso cómo funciona la metodología: Cómo funciona la prueba de contrato Pact.

Nos ofrece todo lo que necesitamos para crear, distribuir y validar contratos dentro de un sistema. Admite la mayoría de los lenguajes habituales y el uso de múltiples lenguajes en una arquitectura. Pact cubre el panorama de contratos impulsados ​​por el cliente y automatiza la mayor parte del proceso.

El flujo comienza cuando el cliente define las pruebas unitarias: ¿qué respuesta espera el cliente en qué estado? Es importante señalar que el cliente debe probar la interacción y no la funcionalidad del proveedor. Si el cliente puede procesar una respuesta pero la prueba falla, probablemente esté probando la funcionalidad del proveedor. Esto debe probarse con pruebas unitarias del proveedor.

Por ejemplo, considera un servicio de validación que devuelve verdadero o falso según la entrada. Un cliente puede probar si el servicio de validación devuelve falso cuando la entrada excede una cierta cantidad de caracteres, pero el servicio de validación es responsable de este límite de caracteres y debe verificarse en sus pruebas unitarias y no en el contrato.

Por cada cambio que realice el cliente, se creará un contrato y se enviará al mediador de Pact. Cuando el proveedor realiza un cambio, todos los contratos del cliente serán recuperados del mediador. Un servidor simulado reproducirá solicitudes de contrato con contratos. Si las respuestas están en línea con el contrato, el proveedor puede implementar los cambios.

Arquitectura de la prueba de contrato con Pact Broker

Architetura de testes de contrato direcionados ao consumidor com Pact Broker

Manos a la obra con Pact

La principal interfaz del consumidor es la clase PactV3 y las exportaciones MatchersV3 del paquete @pact-foundation/pact, de modo que el consumidor pueda escribir una prueba de API y definir sus suposiciones y necesidades de su(s) proveedor(es) de API. Al realizar pruebas unitarias de nuestro cliente API con Pact, se producirá un contrato que podremos compartir con nuestro proveedor para confirmar estas suposiciones y evitar cambios dañinos.

En este ejemplo, probaremos nuestro cliente User API, responsable de comunicarse con UserAPI a través de HTTP. Actualmente, tiene un único método GetUser(id) que devolverá un Usuario.

Las pruebas de pacto tienen algunas propiedades importantes como se demuestra a continuación.

Prueba del lado del consumidor

import { PactV3, MatchersV3 } from '@pact-foundation/pact';


// Criando um 'pacto' entre as duas aplicações na integração que estamos testando

const provider = new PactV3({

  dir: path.resolve(process.cwd(), 'pacts'),

  consumer: 'MyConsumer',

  provider: 'MyProvider',

});


// API do cliente que buscará a API Dog

// Este é o alvo do nosso teste Pact

public getMeDogs = (from: string): AxiosPromise => {

  return axios.request({

    baseURL: this.url,

    params: { from },

    headers: { Accept: 'application/json' },

    method: 'GET',

    url: '/dogs',

  });

};


const dogExample = { dog: 1 };

const EXPECTED_BODY = MatchersV3.eachLike(dogExample);


describe('GET /dogs', () => {

  it('returns an HTTP 200 and a list of docs', () => {

    // Configurando nossas interações esperadas e

    // usando o Pact para simular a API de back-end

    provider

      .given('I have a list of dogs')

      .uponReceiving('a request for all dogs with the builder pattern')

      .withRequest({

        method: 'GET',

        path: '/dogs',

        query: { from: 'today' },

        headers: { Accept: 'application/json' },

      })

      .willRespondWith({

        status: 200,

        headers: { 'Content-Type': 'application/json' },

        body: EXPECTED_BODY,

      });


    return provider.executeTest((mockserver) => {

      // Testando se nosso cliente API se comporta corretamente

      // Observe que configuramos o cliente da API DogService dinamicamente para

      // apontar para o pacto de serviço simulado criado anteriormente


      dogService = new DogService(mockserver.url);

      const response = await dogService.getMeDogs('today')


      // Assert: check the result

      expect(response.data[0]).to.deep.eq(dogExample);

    });

  });

});



Prueba del lado del proveedor

La interfaz principal del proveedor es la clase Verifier del paquete @pact-foundation/pact. Una prueba de proveedor toma uno o más archivos de pacto (contratos) como entrada y Pact verifica si su proveedor cumple con el contrato.

En el caso más simple, puede verificar un proveedor como se muestra a continuación usando un archivo de pacto local, aunque en la práctica normalmente usaría un Pact Broker como se mencionó anteriormente para administrar sus contratos y el flujo de trabajo de CI/CD.

const { Verifier } = require('@pact-foundation/pact');


// 1 - Iniciando o provedor localmente. Certifique-se de eliminar quaisquer dependências externas

server.listen(8081, () => {

  importData();

  console.log('ouvindo o serviço de perfil do animal em http://localhost:8081');

});


// 2 - Verificando se o provedor atende a todas as expectativas do consumidor

describe('Pact Verification', () => {

  it('validates the expectations of Matching Service', () => {

    let token = 'INVALID TOKEN';


    return new Verifier({

      providerBaseUrl: 'http://localhost:8081', // <- localização do seu provedor em execução

      pactUrls: [ path.resolve(process.cwd(), "./pacts/SomeConsumer-SomeProvider.json") ],

    })

      .verifyProvider()

      .then(() => {

        console.log('Verificação do Pacto Concluída com Sucesso!');

      });

  });

});


Conclusión


Las pruebas de contratos de cara al cliente son un concepto muy poderoso que podemos utilizar no solo para verificar la seguridad de los límites del servicio, sino también para diseñar y simplificar nuestras API. Comprender cuáles son los requisitos de los clientes nos ahorra muchas conjeturas al planificar nuestras tareas y escribe nuestro código. Además, es más fácil y rápido que configurar pruebas de integración adecuadas entre servicios, ya que no necesitamos tener dos servicios activos comunicándose entre sí.

Si no controlamos a todos los consumidores de nuestras API, las necesidades exactas de nuestros consumidores pueden perderse en la traducción. Incluso si nuestras pruebas de integración detectan el problema, es posible que no sepamos si detectamos un error del consumidor o si no cumplimos nuestros contratos correctamente.

Probablemente no desee detener un trabajo de CI/CD cuando falla la verificación de un contrato, porque un error tipográfico en una sola simulación de cliente podría impedirle publicar una nueva versión. Sin embargo, puede resultar útil descubrir rápidamente por qué se produjo un error con solo observar el estado de verificación de un contrato.

Pact y Pact Broker son herramientas impresionantes para pruebas de contratos de cara al cliente y pueden ser parte del conjunto de herramientas de cualquier desarrollador que trabaje con sistemas distribuidos. Si tuviera algunas capacidades de aserción más detalladas, podríamos reemplazar algunos casos de prueba que actualmente solo se pueden verificar mediante pruebas de integración complejas.

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.