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.

¿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()
)

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.