Pact Contract Testing โ€” CULTIVA IA

Consumer-Driven Contract Testing para microservicios de la plataforma de automatizacion

โœ“ 8 contratos verificados Node.js 20 / TypeScript Pact Broker self-hosted GitHub Actions CI/CD

๐Ÿ—๏ธ Arquitectura de Microservicios

๐Ÿ“ค
campaign-service
Consumidor
Puerto 3000
GET /api/leads/{id} โœ“ contrato firmado
๐Ÿ‘ฅ
leads-service
Proveedor
Puerto 3456
GET /api/metrics/campaign/{id} โœ“ contrato firmado
๐Ÿ“Š
analytics-service
Proveedor
Puerto 3789
8
Contratos activos
100%
Tests pasando
3
Servicios monitoreados
v2.4.1
Ultimo deploy verificado

๐Ÿ”„ Flujo Consumer-Driven Contract Testing

paso 1
โœ๏ธ
Define expectativas
campaign-svc escribe lo que espera de leads-svc
paso 2
๐Ÿ“„
Genera Pact File
Tests de consumidor crean el JSON de contrato
paso 3
๐Ÿ“ค
Publica en Broker
Pact Broker almacena y versiona los contratos
paso 4
โœ…
Verifica proveedor
leads-svc descarga y valida todos los contratos
paso 5
๐Ÿš€
Can-I-Deploy
CI/CD consulta si es seguro desplegar en prod
Consumer Test
Provider Verification
Pact Broker Config
GitHub Actions CI

Test de Consumidor (campaign-service) tests/leads.consumer.pact.test.ts

tests/leads.consumer.pact.test.ts
TypeScript
// tests/leads.consumer.pact.test.ts
// campaign-service define sus expectativas sobre leads-service.
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { resolve } from 'path';
import { LeadsApiClient } from '../src/api/leadsClient';

const { like, eachLike, string, integer, decimal } = MatchersV3;

const provider = new PactV3({
  consumer: 'campaign-service',
  provider:  'leads-service',
  dir: resolve(__dirname, '../pacts'),
});

describe('Leads API โ€” Consumer Tests', () => {

  it('obtiene un lead por ID con todos sus campos', async () => {
    await provider
      .given('existe un lead con ID lead-001')
      .uponReceiving('peticion de lead por ID')
      .withRequest({
        method: 'GET',
        path:   '/api/leads/lead-001',
        headers: { Accept: 'application/json', Authorization: string('Bearer token') },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: like({
          id:       string('lead-001'),
          name:     string('Maria Garcia'),
          email:    string('maria@empresa.com'),
          score:    decimal(87.5),
          status:   string('qualified'),
          tags:     eachLike(string('newsletter')),
          campaign: string('camp-spring-2026'),
        }),
      })
      .executeTest(async (mockServer) => {
        const client = new LeadsApiClient(mockServer.url);
        const lead   = await client.getLead('lead-001');
        expect(lead.id).toBe('lead-001');
        expect(lead.score).toBeGreaterThan(0);
        expect(lead.status).toBeDefined();
      });
  });

  it('lista leads de una campana con paginacion', async () => {
    await provider
      .given('existen 3 leads para camp-spring-2026')
      .uponReceiving('peticion de leads por campana')
      .withRequest({
        method: 'GET',
        path:   '/api/leads',
        query:  { campaign: 'camp-spring-2026', page: '1' },
      })
      .willRespondWith({
        status: 200,
        body: like({
          data:  eachLike(like({ id: string(), name: string(), score: decimal() })),
          total: integer(3),
          page:  integer(1),
        }),
      })
      .executeTest(async (mockServer) => {
        const client = new LeadsApiClient(mockServer.url);
        const resp   = await client.getLeadsByCampaign('camp-spring-2026');
        expect(resp.data.length).toBeGreaterThan(0);
        expect(resp.total).toBeDefined();
      });
  });

  it('anade etiqueta a un lead existente', async () => {
    await provider
      .given('existe un lead con ID lead-001')
      .uponReceiving('peticion para anadir tag nurturing')
      .withRequest({
        method: 'POST',
        path:   '/api/leads/lead-001/tag',
        body:   { tag: 'nurturing' },
      })
      .willRespondWith({
        status: 200,
        body:   like({ success: true, tags: eachLike(string()) }),
      })
      .executeTest(async (mockServer) => {
        const client = new LeadsApiClient(mockServer.url);
        const res    = await client.addTag('lead-001', 'nurturing');
        expect(res.success).toBe(true);
      });
  });
});

Verificacion del Proveedor (leads-service) tests/leads.provider.pact.test.ts

tests/leads.provider.pact.test.ts
TypeScript
// tests/leads.provider.pact.test.ts
// leads-service verifica que cumple todos los contratos publicados en el broker.
import { Verifier } from '@pact-foundation/pact';
import { startApp } from '../src/app';
import { seedLead, clearLeads } from '../test-utils/database';

describe('Leads Service โ€” Provider Verification', () => {
  let server: any;

  beforeAll(async () => {
    server = await startApp(3456);
  });

  afterAll(async () => {
    await server.close();
  });

  it('cumple todos los contratos del Pact Broker', async () => {
    const verifier = new Verifier({
      providerBaseUrl:          'http://localhost:3456',
      provider:                 'leads-service',
      pactBrokerUrl:            process.env.PACT_BROKER_URL,
      pactBrokerToken:          process.env.PACT_BROKER_TOKEN,
      publishVerificationResult: process.env.CI === 'true',
      providerVersion:          process.env.GIT_COMMIT,
      providerVersionBranch:     process.env.GIT_BRANCH,
      consumerVersionSelectors: [
        { mainBranch: true },
        { deployedOrReleased: true },
      ],
      // Estado de la base de datos antes de cada interaccion
      stateHandlers: {
        'existe un lead con ID lead-001': async () => {
          await seedLead({
            id:       'lead-001',
            name:     'Maria Garcia',
            email:    'maria@empresa.com',
            score:    87.5,
            status:   'qualified',
            tags:     ['newsletter', 'webinar'],
            campaign: 'camp-spring-2026',
          });
        },
        'existen 3 leads para camp-spring-2026': async () => {
          await seedLead({ id: 'lead-001', campaign: 'camp-spring-2026', score: 87.5 });
          await seedLead({ id: 'lead-002', campaign: 'camp-spring-2026', score: 62.0 });
          await seedLead({ id: 'lead-003', campaign: 'camp-spring-2026', score: 45.3 });
        },
      },
    });

    await verifier.verifyProvider();
  });
});
http://pact-broker.cultiva-ia.internal:9292
Matriz de Contratos
Participantes
Entornos
Webhooks
๐Ÿ“Š Matriz de Verificacion de Contratos
Consumidor Proveedor Interaccion Version consumidor Version proveedor Resultado Fecha
campaign-service leads-service GET /api/leads/{id} 2.4.1 / main 3.1.0 / main PASS 2026-06-16 09:14
campaign-service leads-service GET /api/leads?campaign={id} 2.4.1 / main 3.1.0 / main PASS 2026-06-16 09:14
campaign-service leads-service POST /api/leads/{id}/tag 2.4.1 / main 3.1.0 / main PASS 2026-06-16 09:14
campaign-service analytics-service GET /api/metrics/campaign/{id} 2.4.1 / main 1.8.2 / main PASS 2026-06-16 09:15
campaign-service analytics-service GET /api/metrics/campaign/{id}/breakdown 2.4.1 / main 1.8.2 / main PASS 2026-06-16 09:15

โš™๏ธ Pipeline GitHub Actions โ€” Ultimo Run (main @ 2.4.1)

๐Ÿ”ง
Checkout & Install
โœ“ 34s
โ†’
๐Ÿงช
Consumer Tests
โœ“ 12s
โ†’
๐Ÿ“ค
Publish Pacts
โœ“ 4s
โ†’
๐Ÿ”
Can-I-Deploy
โœ“ 3s
โ†’
๐Ÿš€
Deploy Prod
โœ“ 58s
Terminal โ€” can-i-deploy check
$ npx pact-broker can-i-deploy \
--pacticipant campaign-service \
--version a3f8d21c \
--to-environment production \
--broker-base-url $PACT_BROKER_URL \
--broker-token $PACT_BROKER_TOKEN
INFO Checking if campaign-service@a3f8d21c can be deployed to production
INFO Loading matrix for campaign-service, latest version and its dependencies
CONSUMER PROVIDER STATUS
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€
campaign-service leads-service โœ“ verified
campaign-service analytics-service โœ“ verified
โœ“ Computer says yes \o/
campaign-service v2.4.1 (a3f8d21c) can be deployed to production.
All contracts satisfied.
$ npx pact-broker record-deployment \
--pacticipant campaign-service --version a3f8d21c \
--environment production
โœ“ Recorded deployment of campaign-service v2.4.1 to production

GitHub Actions โ€” Pipeline completo .github/workflows/pact-consumer.yml

.github/workflows/pact-consumer.yml
YAML
# .github/workflows/pact-consumer.yml
# Pipeline de campaign-service: tests de contrato + deploy seguro a produccion.
name: Consumer Contract Tests โ€” campaign-service
on:
  push:
    branches: [main, 'feature/**']
  pull_request:
    branches: [main]

env:
  PACT_BROKER_URL:   ${{ secrets.PACT_BROKER_URL }}
  PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
  GIT_COMMIT:        ${{ github.sha }}
  GIT_BRANCH:        ${{ github.ref_name }}

jobs:
  pact-consumer:
    name: Contract Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm

      - name: Instalar dependencias
        run: npm ci

      - name: Ejecutar consumer tests (genera pacts/)
        run: npm test -- --testPathPattern="\.pact\.test\."

      - name: Publicar contratos en Pact Broker
        run: |
          npx pact-broker publish pacts/ \
            --consumer-app-version $GIT_COMMIT \
            --branch $GIT_BRANCH

      - name: Can I deploy? (verifica contratos satisfechos)
        run: |
          npx pact-broker can-i-deploy \
            --pacticipant campaign-service \
            --version $GIT_COMMIT \
            --to-environment production

  deploy:
    name: Deploy a Produccion
    needs: pact-consumer
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: ./scripts/deploy.sh production

      - name: Registrar deploy en Pact Broker
        run: |
          npx pact-broker record-deployment \
            --pacticipant campaign-service \
            --version $GIT_COMMIT \
            --environment production