Contract testing: cómo detectar incompatibilidades entre APIs antes del despliegue
Un equipo modifica una respuesta de una API para hacerla más consistente: cambia el campo customerName por name. El servicio proveedor pasa sus pruebas unitarias, los endpoints responden correctamente y el despliegue parece seguro. Sin embargo, una aplicación consumidora todavía busca customerName y empieza a mostrar errores en producción.
Este tipo de incompatibilidad aparece cuando la validación se concentra en cada servicio por separado, pero no verifica las expectativas que existen entre consumidores y proveedores. Las pruebas de contrato ayudan a detectar ese problema antes del despliegue, validando que ambas partes siguen cumpliendo un acuerdo verificable sobre la estructura y el comportamiento de una integración.
En este artículo veremos cómo funcionan las pruebas de contrato consumidor-proveedor, en qué se diferencian de las pruebas de endpoints y de las pruebas end-to-end, cómo incorporarlas a un pipeline de CI y cuáles son sus límites.
Qué problema resuelven las pruebas de contrato
Una API compartida no es solamente una colección de rutas HTTP. También define expectativas sobre:
- Los métodos y recursos disponibles.
- Los parámetros requeridos y opcionales.
- Los códigos de estado.
- La estructura de las respuestas.
- Los tipos y formatos de los campos.
- Las condiciones de error.
- El comportamiento necesario para que el consumidor pueda procesar la respuesta.
Cuando un consumidor depende de un proveedor, cualquier cambio incompatible puede romper la integración aunque el proveedor siga funcionando correctamente desde su propia perspectiva.
Un cambio aparentemente menor
Supongamos que un servicio de clientes devuelve inicialmente:
{
"id": "c-123",
"customerName": "Acme"
}
Una aplicación de facturación utiliza el campo customerName para generar el encabezado de sus comprobantes. El equipo del servicio de clientes decide renombrarlo:
{
"id": "c-123",
"name": "Acme"
}
Desde el punto de vista del proveedor, la respuesta sigue siendo válida y contiene el nombre del cliente. Desde el punto de vista del consumidor, el cambio elimina un campo que necesita. Si la modificación se despliega sin coordinación, la aplicación de facturación puede producir datos incompletos o fallar al procesar la respuesta.
Una prueba de contrato habría expresado la expectativa del consumidor: para una determinada solicitud, la respuesta debe incluir un campo customerName de tipo texto. El cambio del proveedor habría sido detectado durante la validación, antes de llegar al entorno de producción.
Cómo funciona el contrato entre consumidor y proveedor
El contrato representa las expectativas relevantes del consumidor sobre una integración. No necesariamente describe cada detalle interno del proveedor ni todos los casos posibles de la API. Su objetivo es verificar el comportamiento que una relación concreta necesita.
El flujo habitual incluye dos responsabilidades:
- El consumidor define sus expectativas: especifica qué solicitud realiza y qué parte de la respuesta necesita.
- El proveedor verifica esas expectativas: ejecuta el contrato contra su implementación y confirma que puede cumplirlo.
Ejemplo de una expectativa verificable
El consumidor podría establecer una expectativa conceptual como la siguiente:
Solicitud:
GET /customers/c-123
Respuesta esperada:
- código HTTP 200
- campo "id" presente y de tipo texto
- campo "customerName" presente y de tipo texto
El contrato no tiene que exigir valores exactos para todos los campos. En muchos casos es suficiente comprobar que existe la estructura necesaria y que los datos cumplen ciertas propiedades. Esto permite que el proveedor utilice distintos identificadores o nombres reales en cada ejecución sin invalidar la prueba.
También es posible expresar expectativas sobre errores. Por ejemplo, el consumidor puede necesitar que una solicitud con un identificador inexistente devuelva un estado concreto y una estructura de error procesable. La precisión debe ser suficiente para proteger la integración, pero no tan estricta que convierta cualquier cambio interno en una falsa incompatibilidad.
Pruebas de contrato frente a pruebas de endpoints y end-to-end
Estas técnicas se complementan, pero responden preguntas diferentes.
Pruebas aisladas de endpoints
Una prueba de endpoint suele validar un recurso del proveedor de forma independiente. Puede comprobar que una ruta devuelve un código de estado, que una regla de negocio funciona o que una entrada inválida genera un error.
Es útil para verificar el comportamiento del servicio, pero puede no representar las expectativas reales de cada consumidor. Una prueba del proveedor podría aceptar tanto name como customerName, mientras que el consumidor solamente sabe procesar uno de ellos.
Pruebas de contrato
Las pruebas de contrato se enfocan en la relación entre las partes. Preguntan si el proveedor sigue cumpliendo las expectativas que un consumidor declaró como necesarias.
Su alcance suele ser menor que el de una prueba end-to-end, pero es más específico para detectar incompatibilidades de integración. En lugar de recorrer todo un flujo empresarial, valida el acuerdo entre servicios.
Pruebas end-to-end
Una prueba end-to-end recorre varios componentes, por ejemplo una aplicación web, un backend, un servicio de clientes, una base de datos y un sistema de pagos. Sirve para comprobar que un flujo completo funciona en conjunto.
Su valor es importante, pero normalmente implica más infraestructura, más tiempo de ejecución y más puntos de fallo. Además, cuando falla puede ser más difícil identificar si el problema está en el consumidor, el proveedor, los datos o la configuración del entorno.
Una estrategia equilibrada puede utilizar pruebas aisladas para la lógica de cada servicio, pruebas de contrato para las integraciones críticas y un conjunto acotado de pruebas end-to-end para los flujos de negocio más relevantes.
Cómo incorporar contract testing al ciclo de CI
Para que las pruebas de contrato prevengan incompatibilidades, deben ejecutarse antes de que un cambio incompatible pueda desplegarse. El diseño exacto depende de la arquitectura y de las herramientas disponibles, pero el flujo conceptual puede ser el siguiente.
1. Identificar consumidores y dependencias
Antes de crear contratos, conviene saber qué servicios, aplicaciones o equipos consumen cada API. Esta información puede obtenerse de repositorios, configuraciones, registros de tráfico, documentación y conversaciones con los equipos responsables.
El inventario debe distinguir entre consumidores activos, consumidores en migración y consumidores que ya no deberían utilizar una API. Un contrato obsoleto puede bloquear cambios legítimos si nadie revisa su vigencia.
2. Seleccionar integraciones críticas
No es necesario comenzar con todas las APIs. Es razonable priorizar aquellas cuyo cambio puede afectar procesos importantes, que tienen muchos consumidores o que actualmente presentan incidentes de compatibilidad.
3. Definir expectativas desde el consumidor
El consumidor debe declarar qué necesita realmente. Si una aplicación solamente utiliza id y customerName, no debería exigir que el proveedor mantenga sin motivo todos los campos de una respuesta completa.
Los contratos pueden versionarse junto con el código del consumidor o almacenarse en un repositorio accesible para ambas partes. Lo importante es que formen parte del proceso de revisión y no sean un documento separado que rápidamente quede desactualizado.
4. Validar el proveedor en el pipeline
El pipeline del proveedor debe ejecutar las verificaciones contra una instancia controlada del servicio. La validación puede realizarse durante la construcción, en un entorno temporal o como una comprobación previa a la promoción, según las características del sistema.
Si el proveedor elimina un campo requerido, cambia su tipo o modifica una condición que el consumidor necesita, la ejecución debe indicar una incompatibilidad y detener el avance del cambio, salvo que exista una decisión explícita para aceptar el riesgo.
5. Coordinar la compatibilidad durante la evolución
Cuando una API necesita cambiar, una estrategia habitual es introducir primero la nueva forma sin eliminar inmediatamente la anterior. Por ejemplo, el proveedor podría publicar temporalmente ambos campos:
{
"id": "c-123",
"customerName": "Acme",
"name": "Acme"
}
El consumidor se migra para utilizar name, se actualizan sus expectativas y, después de confirmar que no existen consumidores dependientes del campo anterior, se evalúa su eliminación.
Este patrón no siempre es viable ni conveniente. Mantener dos formas de una respuesta aumenta la complejidad y puede prolongar una migración. La decisión debe considerar el número de consumidores, el control que existe sobre ellos y el costo de mantener compatibilidad.
Qué cambios suelen detectar y cuáles pueden pasar inadvertidos
Las pruebas de contrato son especialmente útiles para identificar cambios estructurales o semánticos que afectan directamente a un consumidor.
- Eliminar un campo requerido.
- Renombrar una propiedad utilizada por el consumidor.
- Cambiar el tipo de un campo, por ejemplo de texto a objeto.
- Modificar un código de estado que el consumidor trata de forma específica.
- Cambiar el formato de una fecha o de un identificador.
- Alterar la estructura de un error que el consumidor necesita interpretar.
- Convertir un campo opcional en una condición que el consumidor no puede manejar.
Sin embargo, un contrato puede no detectar todos los problemas. Por ejemplo, podría existir un campo con el tipo correcto pero con un significado diferente. También pueden pasar inadvertidos cambios relacionados con rendimiento, límites de uso, autorización, consistencia eventual, orden de eventos o reglas de negocio que no estén expresadas en el contrato.
Por esta razón, el contrato debe proteger las expectativas relevantes, pero no reemplaza otras formas de validación.
Errores frecuentes al implementar pruebas de contrato
Confundir el contrato con la documentación
Una especificación de API puede describir las capacidades del proveedor, mientras que un contrato de consumidor expresa las necesidades concretas de una integración. Ambos artefactos pueden relacionarse, pero no son necesariamente equivalentes.
Verificar demasiados detalles
Si el contrato exige valores exactos, ordenamientos no relevantes o campos internos que el consumidor no utiliza, puede generar falsos positivos. Esto dificulta la evolución de la API y reduce la confianza en las pruebas.
Verificar muy poco
Un contrato que solamente confirma que la ruta responde con estado 200 no protege la estructura que el consumidor necesita. Debe incluir los campos, tipos y condiciones relevantes para el flujo real.
Dejar contratos abandonados
Cuando un consumidor se retira o cambia de versión, sus contratos deben revisarse. De lo contrario, el proveedor puede quedar obligado a mantener una compatibilidad que ya no tiene valor.
Usarlos como sustituto de pruebas funcionales
Una prueba de contrato puede confirmar que existe un campo total, pero no necesariamente que el cálculo del total sea correcto. Las reglas de negocio, la persistencia, la seguridad y los flujos completos requieren pruebas adicionales.
Cuándo utilizar contract testing y cuáles son sus límites
Las pruebas de contrato suelen ser una buena opción cuando existen varios servicios independientes, equipos que despliegan a ritmos diferentes o integraciones que deben evolucionar sin coordinación manual en cada cambio.
También son útiles en arquitecturas con APIs HTTP, mensajería o eventos, siempre que sea posible expresar las expectativas del consumidor sobre el mensaje, sus campos, sus claves o sus condiciones de procesamiento. En sistemas event-driven, por ejemplo, el contrato puede describir la estructura y compatibilidad de un evento publicado para sus consumidores.
Puede no ser la técnica principal cuando:
- El sistema es pequeño y todos sus componentes se despliegan como una única unidad.
- No existe una separación clara entre consumidores y proveedores.
- La integración depende principalmente de comportamiento visual o interacción humana.
- El problema central está en rendimiento, seguridad o disponibilidad, aspectos que requieren otras pruebas.
- Los consumidores cambian constantemente y todavía no existe una expectativa estable que valga la pena formalizar.
El valor del contract testing depende también de la gobernanza técnica. Un contrato fallido debe generar una decisión: corregir el proveedor, actualizar el consumidor, introducir una transición compatible o aceptar conscientemente una ruptura. Si el equipo ignora siempre el resultado del pipeline, la prueba deja de ser una protección efectiva.
Conclusión: validar el acuerdo antes de desplegar el cambio
Las pruebas de contrato permiten comprobar que una API sigue cumpliendo las expectativas reales de sus consumidores. Su principal aporte no es probar todo el sistema, sino detectar temprano incompatibilidades como campos eliminados, tipos modificados, códigos de estado inesperados o estructuras de error incompatibles.
Para aplicarlas con criterio, conviene comenzar por integraciones críticas, definir expectativas desde el consumidor, ejecutar la verificación en CI y mantener los contratos alineados con los consumidores activos. Estas pruebas deben complementar, no reemplazar, las pruebas unitarias, de endpoints, funcionales, de seguridad, rendimiento y end-to-end.
Antes de cambiar una API compartida, identifica las expectativas de sus consumidores y evalúa contratos verificables en CI. Esta práctica ayuda a convertir la compatibilidad entre servicios en una condición comprobable del proceso de entrega, en lugar de descubrirla después del despliegue.
En equipos que necesitan fortalecer su estrategia de QA, backend o arquitectura de integraciones, este análisis puede formar parte de una capacitación o asesoría técnica de Mentores Tech sobre calidad de software y arquitectura moderna.
