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:
- Comienza con flujos críticos de usuario (login, obtención de datos)
- Configura Pact Broker para gestión de contratos
- Integra en pipeline CI/CD
- Añade verificaciones can-i-deploy antes de releases
- 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.