TL;DR

  • Los contratos dirigidos por el consumidor permiten a equipos móviles definir expectativas de API sin esperar al backend
  • Los tests Pact se ejecutan en milisegundos vs segundos en tests de integración, detectando cambios disruptivos antes del despliegue
  • La verificación can-i-deploy es tu red de seguridad—nunca despliegues sin ella

Ideal para: Equipos móviles consumiendo APIs de microservicios, equipos con cambios frecuentes de API

Omitir si: Backend monolítico único, APIs estables con cambios raros

Tiempo de lectura: 15 minutos

El contract testing de API asegura que las aplicaciones móviles y servicios backend se comuniquen correctamente sin requerir tests de integración completos. Este enfoque detecta cambios disruptivos temprano, permite despliegue independiente y mantiene compatibilidad retroactiva entre versiones de API. Mientras que los fundamentos de testing de API se centran en validar endpoints individuales, el contract testing adopta un enfoque dirigido por el consumidor para garantizar una integración fluida.

Para maximizar la efectividad del contract testing, es esencial comprender cómo se integra con otras prácticas de testing. Los mock servers en desarrollo móvil proporcionan la base para simular providers durante el desarrollo, mientras que las estrategias de versionado de API definen cómo evolucionar contratos sin romper clientes existentes.

¿Qué es Contract Testing?

El contract testing verifica que dos sistemas separados (consumidor y proveedor) estén de acuerdo en el formato de los mensajes que intercambian. A diferencia de los tests end-to-end, los contract tests se ejecutan independientemente para cada servicio.

Testing de Integración Tradicional vs Contract Testing

Testing de Integración Tradicional:

App Móvil → Stack Backend Completo → Base de Datos
- Lento (segundos a minutos)
- Inestable (red, problemas de entorno)
- Requiere infraestructura completa
- Difícil reproducir casos extremos

Contract Testing:

App Móvil → Contract Stub (Pact Mock)
Backend API → Contract Verification (Pact Provider)
- Rápido (milisegundos)
- Confiable (sin dependencias de red)
- Se ejecuta en pipelines CI/CD
- Simulación fácil de casos extremos

Conceptos Básicos

Contratos Dirigidos por el Consumidor

El consumidor (app móvil) define expectativas para el comportamiento del proveedor (backend API). El proveedor debe honrar estos contratos. Esto es particularmente crucial en estrategias modernas de testing móvil donde las apps deben adaptarse a servicios backend en evolución.

Beneficios:

  • Equipos móviles no bloqueados por retrasos del backend
  • Cambios de API validados antes del despliegue
  • Comunicación clara entre equipos
  • Estrategias de versionado y deprecación

Flujo de Trabajo Pact

1. Consumidor (Móvil) escribe tests Pact
2. Genera archivo de contrato Pact (JSON)
3. Contrato publicado en Pact Broker
4. Proveedor (Backend) verifica contrato
5. Resultados publicados en Pact Broker
6. Verificación Can-I-Deploy antes del release

Pact para Aplicaciones Móviles

Implementación Android

Setup (build.gradle.kts):

dependencies {
    testImplementation("au.com.dius.pact.consumer:junit5:4.6.1")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}

Test Pact del Consumidor (UserApiPactTest.kt):

import au.com.dius.pact.consumer.dsl.PactBuilder
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt
import au.com.dius.pact.consumer.junit5.PactTestFor
import au.com.dius.pact.core.model.V4Pact
import au.com.dius.pact.core.model.annotations.Pact
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(PactConsumerTestExt::class)
@PactTestFor(providerName = "UserService")
class UserApiPactTest {

    @Pact(consumer = "MobileApp")
    fun getUserByIdPact(builder: PactBuilder): V4Pact {
        return builder
            .usingLegacyDsl()
            .given("usuario 123 existe")
            .uponReceiving("una petición para usuario 123")
            .path("/api/users/123")
            .method("GET")
            .headers(mapOf("Accept" to "application/json"))
            .willRespondWith()
            .status(200)
            .headers(mapOf("Content-Type" to "application/json"))
            .body("""
                {
                  "id": 123,
                  "username": "juan_perez",
                  "email": "juan@ejemplo.com",
                  "createdAt": "2024-01-15T10:30:00Z"
                }
            """.trimIndent())
            .toPact()
            .asV4Pact().get()
    }

    @Test
    @PactTestFor(pactMethod = "getUserByIdPact", port = "8080")
    fun testGetUserById() {
        val apiClient = ApiClient("http://localhost:8080")

        runBlocking {
            val user = apiClient.getUser(123)
            assertEquals(123, user.id)
            assertEquals("juan_perez", user.username)
        }
    }
}

Verificación del Proveedor (Backend)

Verificación Spring Boot Provider

La verificación del proveedor es esencial en arquitectura de API de microservicios, donde múltiples servicios deben mantener compatibilidad de contratos.

Setup (build.gradle.kts):

dependencies {
    testImplementation("au.com.dius.pact.provider:junit5spring:4.6.1")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Test del Proveedor (UserServiceProviderTest.kt):

@SpringBootTest
@AutoConfigureMockMvc
@Provider("UserService")
@PactBroker(host = "pact-broker.ejemplo.com")
class UserServiceProviderTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @Autowired
    private lateinit var userRepository: UserRepository

    @BeforeEach
    fun setUp(context: PactVerificationContext) {
        context.target = MockMvcTestTarget(mockMvc)
    }

    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider::class)
    fun pactVerificationTestTemplate(context: PactVerificationContext) {
        context.verifyInteraction()
    }

    @State("usuario 123 existe")
    fun userExistsState() {
        val user = User(
            id = 123,
            username = "juan_perez",
            email = "juan@ejemplo.com"
        )
        userRepository.save(user)
    }
}

Pact Broker Setup

Configuración Docker Compose

version: '3'

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: pact
      POSTGRES_PASSWORD: pact
      POSTGRES_DB: pact_broker

  pact-broker:
    image: pactfoundation/pact-broker:latest
    ports:

      - "9292:9292"
    depends_on:

      - postgres
    environment:
      PACT_BROKER_DATABASE_USERNAME: pact
      PACT_BROKER_DATABASE_PASSWORD: pact
      PACT_BROKER_DATABASE_HOST: postgres

Versionado y Compatibilidad Retroactiva

Manejo de Cambios de API

Cambio No Disruptivo (Añadir Campo Opcional):

// Contrato antiguo
{
  "id": 123,
  "username": "juan_perez",
  "email": "juan@ejemplo.com"
}

// Nuevo contrato (compatible retroactivamente)
{
  "id": 123,
  "username": "juan_perez",
  "email": "juan@ejemplo.com",
  "avatar": "https://cdn.ejemplo.com/avatar.jpg" // Opcional, nuevo campo
}

Cambio Disruptivo (Eliminar Campo):

// El proveedor debe soportar contrato antiguo durante período de transición
@GetMapping("/api/users/{id}")
fun getUser(@PathVariable id: Long, @RequestHeader("API-Version") version: String?): UserResponse {
    val user = userRepository.findById(id)

    return when (version) {
        "v1" -> UserResponseV1(user) // Incluye campos deprecados
        "v2", null -> UserResponseV2(user) // Nuevo contrato
        else -> throw UnsupportedVersionException()
    }
}

Mejores Prácticas

1. Probar Escenarios Reales

@Pact(consumer = "MobileApp")
fun rateLimitedRequestPact(builder: PactBuilder): V4Pact {
    return builder
        .usingLegacyDsl()
        .given("límite de tasa excedido para usuario 123")
        .uponReceiving("petición que activa límite de tasa")
        .path("/api/products")
        .method("GET")
        .willRespondWith()
        .status(429)
        .body("""
            {
              "error": "rate_limit_exceeded",
              "message": "Demasiadas peticiones",
              "retryAfter": 60
            }
        """.trimIndent())
        .toPact()
        .asV4Pact().get()
}

2. Usar Matchers para Contratos Flexibles

Los matchers permiten validación flexible de contratos, similar a cómo REST Assured maneja aserciones de API, pero a nivel de contrato en lugar de runtime.

import au.com.dius.pact.consumer.dsl.LambdaDsl.*

.body(
    newJsonBody { obj ->
        obj.numberType("id", 123)
        obj.stringType("username", "juan_perez")
        obj.stringMatcher("email", ".*@ejemplo\\.com", "juan@ejemplo.com")
        obj.datetime("createdAt", "yyyy-MM-dd'T'HH:mm:ss'Z'")
    }.build()
)

Enfoques Asistidos por IA

El contract testing en 2026 se beneficia de herramientas asistidas por IA para generación y análisis de contratos.

Lo que la IA hace bien:

  • Generar tests Pact iniciales desde especificaciones OpenAPI/Swagger
  • Identificar casos extremos faltantes en contratos existentes (404s, rate limits, errores de validación)
  • Sugerir matchers para validación flexible de contratos
  • Detectar potenciales cambios disruptivos analizando diferencias de contratos

Lo que aún necesita humanos:

  • Decidir qué escenarios del consumidor son críticos para producción
  • Diseñar setup de provider states para datos de prueba realistas
  • Balancear rigidez vs flexibilidad de contratos
  • Gestionar evolución de contratos entre versiones de API

Prompts útiles:

Analiza esta especificación OpenAPI y genera tests Pact de consumidor para una
app Android cubriendo: respuestas exitosas, 404 not found, 401 unauthorized,
422 errores de validación, y 429 rate limiting.
Revisa mis contratos Pact existentes e identifica brechas en cobertura de
manejo de errores. Sugiere provider states adicionales para testing realista.

Cuándo Usar Contract Testing

Contract testing funciona mejor cuando:

  • Múltiples clientes móviles (iOS, Android, Web) consumen la misma API
  • Los equipos de backend y móvil despliegan independientemente
  • La API cambia frecuentemente durante desarrollo activo
  • Los tests de integración son lentos o inestables
  • Necesitas confianza antes de desplegar consumidor o proveedor

Considera alternativas cuando:

  • Consumidor único con API estable (tests de integración pueden bastar)
  • Arquitectura monolítica con frontend acoplado
  • Fase de prototipado donde contratos cambian diariamente
  • APIs externas que no controlas (usa servidores stub en su lugar)
EscenarioRecomendación
3+ consumidores, releases frecuentesContract testing esencial
Consumidor único, API estableTests de integración pueden ser suficientes
Greenfield con APIs evolucionandoComienza contratos temprano, itera
Monolito legacyAñade contratos al modularizar

Midiendo el Éxito

MétricaAntes de Contract TestingObjetivoCómo Rastrear
Fallos de integración en prodVariable0Monitoreo de errores en producción
Tiempo para detectar cambios disruptivosDías (en staging)Minutos (en CI)Duración del pipeline CI
Confianza en release móvilBaja (testing manual)Alta (automatizado)Encuesta de equipo
Frecuencia de despliegue backendSemanalDiarioLogs de despliegue

Señales de advertencia de que no funciona:

  • Contratos pasan pero integración aún falla (contratos muy laxos)
  • Cada cambio de backend rompe contratos (contratos muy estrictos)
  • Equipos ignorando resultados de can-i-deploy
  • Provider states no coinciden con patrones de datos de producción

Conclusión

El contract testing con Pact proporciona:

  • Retroalimentación Rápida: Detecta cambios disruptivos en segundos
  • Despliegue Independiente: Equipos móviles y backend trabajan en paralelo
  • Confianza: Despliega sin miedo a fallos de integración
  • Documentación: Contratos API vivos

Hoja de Ruta de Implementación:

  1. Comienza con flujos críticos de usuario (login, obtención de datos)
  2. Configura Pact Broker para gestión de contratos
  3. Integra en pipeline CI/CD
  4. Añade verificaciones can-i-deploy antes de releases
  5. Expande cobertura a todas las interacciones de API

El contract testing es esencial para apps móviles que consumen microservicios, permitiendo iteración rápida mientras mantiene estabilidad.

Ver También

Recursos Oficiales