Gestionando el Estado del Sistema en Contract Testing con Pact
15 de septiembre de 2024
El contract testing es una técnica de pruebas de software que verifica la integridad de las interacciones entre los componentes de un sistema según contratos predefinidos. Esta metodología asegura que los cambios en un servicio no afecten negativamente a otros.
Sin embargo, uno de los mayores desafíos en su implementación es la gestión del estado inicial del sistema antes de ejecutar las pruebas.
Este artículo explorará la importancia de este aspecto y las diferentes técnicas para abordarlo eficazmente.
¿Por qué es interesante?
La gestión del estado del sistema en contract testing es esencial por varias razones:
1- Reproducibilidad: Garantiza que las pruebas sean consistentes y reproducibles, independientemente del entorno o el momento en que se ejecuten.
2- Aislamiento: Permite aislar los componentes bajo prueba, reduciendo las interferencias de factores externos.
3- Confiabilidad: Aumenta la confianza en los resultados de las pruebas, ya que se basan en un estado conocido y controlado.
4- Eficiencia: Facilita la automatización y la ejecución rápida de las pruebas, al no depender de configuraciones complejas del entorno.
Profundizando
Una de las mayores ventajas del uso de Pact para contract testing es que, gracias a su sistema de mockeo de peticiones, es capaz de generar tráfico HTTP tanto en la parte del proveedor como del consumidor. Con esto, conseguimos validar ambas partes de manera independiente.
Para ejecutar las verificaciones del lado del proveedor, es fundamental que esté preparado para recibir peticiones en el momento de la prueba, es decir, arrancado y funcionando. Pact se encargará de validar que las respuestas del proveedor cumplan el contrato previamente establecido.
Es aquí donde se plantea un desafío significativo de hacer una gestión adecuada de los datos y el estado del sistema durante la prueba. Estos factores son determinantes para la validación de los contratos.
En este contexto, exploraremos diversas técnicas para abordar eficazmente estos desafíos de gestión de estado y datos.Un aspecto clave es determinar el nivel de realismo necesario en el estado del sistema. Por un lado, un estado más realista puede proporcionar pruebas más confiables, pero por otro, puede hacer que las pruebas sean más lentas y difíciles de mantener.
Contexto
Para ilustrar de manera concreta el concepto que estamos abordando en este artículo, examinemos un ejemplo práctico en Java, donde la parte consumidora espera que el estudiante con ID 1, exista y el código que debe implementar el proveedor.
Consumidor
@Pact(consumer \= "Student", provider \= "Provider")
public RequestResponsePact getStudent(PactDslWithProvider builder) {
return builder.given("Student 1 exists")
.uponReceiving("get student with ID 1")
.path("/student/1")
.method("GET")
.willRespondWith()
.status(200)
.body(newJsonBody(object \-> {
object.stringType("id", "1");
object.stringValue("name", "Sara");
object.stringType("email", "sara.test@test.com");
object.numberType("studentNumber", 23);
}).build()).toPact();
}
Proveedor
@State("student 1 exist")
public void noStudentExist() {
}
*Pact siempre nos obliga a declarar la función para gestionar el estado que establece el consumidor, pero no estamos obligados a implementarla.
En los siguientes casos, veremos qué modificaciones hay que realizar en esta función para que tengan efecto.
Técnicas
A continuación evaluaremos diferentes aproximaciones para gestionar el estado inicial del sistema previo a la prueba, cada una de ellas con sus ventajas e inconvenientes.
-
Consideraciones previas
Como ya hemos comentado en otro artículo, existen diferencias conceptuales entre contract testing y testing funcional. El objetivo del primero es validar en fases tempranas y, de manera automática, que consumidor y proveedor son capaces de comunicarse correctamente entre sí, dado que están cumpliendo las reglas previamente acordadas.
Sin embargo, el testing funcional va un poco más allá, puesto que, además de validar que la comunicación es correcta, verifica que los componentes se comportan como se espera, es decir, “hacen lo que tienen que hacer”. Esto incluye tanto comprobar el contenido de las respuestas, flujos de datos y efectos laterales esperados.
Esta distinción, puede parecer un simple detalle, pero tiene un impacto significativo en la estrategia de testing. En lugar de centrarse en los valores concretos de los mensajes, se prioriza en su “forma”, es decir, esquema, tipo de datos, etc.
Est enfoque jugará un papel fundamental a la hora de evaluar los pros y contras en cada una de las aproximaciones.
-
Uso de origen de datos real
Supongamos que nuestro servicio consume información de una base de datos de desarrollo. Con este enfoque, las validaciones de contratos se harían utilizando ese mismo origen de datos.
✅Pros:
-
Sin configuración específica para lanzar las pruebas.
-
Es posible que no haga falta un script de carga previa de datos.
-
Alta fidelidad con el entorno de producción.
-
Detección de problemas relacionados con datos reales.
❌ Contras:
-
Las pruebas están muy acopladas a los datos existentes en la base de datos.
-
Posibles colisiones con otros usuarios o pruebas.
-
El estado inicial puede no ser conocido, lo que dificulta la reproducibilidad de las pruebas.
-
Se dificulta la gestión o borrado de datos.
-
Dependencia de la disponibilidad de recursos.
-
Posibles problemas de rendimiento y velocidad de ejecución.
-
Dificultad para controlar casos de límite o situaciones específicas.
Código en proveedor
En el escenario planteado en el ejemplo, la aplicación arranca con una configuración adecuada que establece la conexión con la base de datos apropiada. Esta configuración inicial elimina la necesidad de realizar ajustes o modificaciones específicas.
@State("student 1 exist")
public void noStudentExist() {
}
Conclusión
Aunque este enfoque sea el que requiere menor configuración, es el menos aconsejable. A la larga, los problemas que genera esta técnica superan, en mucho, las ventajas iniciales.
-
Dependencias dockerizadas o base de datos en memoria
Las dependencias dockerizadas utilizan la tecnología de contenedores para encapsular y aislar los componentes del sistema necesarios para las pruebas de contrato. Esta técnica permite crear entornos consistentes y reproducibles, donde cada servicio o dependencia se ejecuta en su propio contenedor. Docker facilita la configuración y el control preciso del estado del sistema, ofreciendo un equilibrio entre el realismo y la facilidad de gestión.
Aunque las bases de datos en memoria y los entornos dockerizados no son soluciones completamente equivalentes, en este contexto pueden tener pros y contras muy similares.
✅Pros:
-
Entorno consistente y reproducible.
-
Fácil de configurar y compartir entre equipos.
-
Buen equilibrio entre realismo y control.
-
Entorno estanco y portable.
-
Facilita la depuración de pruebas.
-
Sistema de login.
❌Contras:
-
La carga inicial de datos puede resultar complicada.
-
El mantenimiento del script inicial de datos puede resultar laborioso.
-
Obliga a mantener sincronización constante entre los cambios en base de datos y el script de carga de datos.
-
Puede requerir recursos significativos de hardware.
-
Posible complejidad en la orquestación de múltiples contenedores.
-
Tiempo de inicio potencialmente largo para pruebas rápidas.
Código en proveedor
Para este ejemplo, podemos utilizar TestContainers. De esta manera, podemos ver como para cada prueba:
1- Se genera una instancia de Mongodb.
2- Se inyectan las dependencias en el controlador.
3- Se configura la conexión con el contenedor.
4- Se insertar un estudiante con ID 1 antes de la prueba.
@Testcontainers
class ProviderContractTest {
@Container
static MongoDBContainer mongoDBContainer \= new MongoDBContainer("mongo:4.4.6");
@Autowired
private StudentRepository studentRepository;
@Autowired
private StudentController studentController;
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
}
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new MockMvcTestTarget(studentController));
studentRepository.deleteAll();
}
@State("Student 1 exists")
void student1Exists() {
Student student \= new Student();
student.setId("1");
student.setName("Sara");
student.setEmail("sara.test@example.com");
student.setStudentNumber(99);
studentRepository.save(student);
}
Conclusión
Este enfoque híbrido ofrece ventajas significativas cuando se requiere un conjunto de datos representativo para las pruebas, ya que nos evitaría la configuración de “cirujano” que requiere el mocking. Por otra parte, también obliga a una sincronización constante del código y los cambios en base de datos.
-
Dependencias mockeadas por código
Las dependencias mockeadas por código implican la creación de versiones simuladas o "mock" de los servicios y componentes externos con los que interactúa el sistema bajo prueba. Estas simulaciones se programan para responder de manera predefinida a las solicitudes, permitiendo un control total sobre el comportamiento de las dependencias. Esta técnica es especialmente útil para probar casos límite y situaciones difíciles de reproducir con sistemas reales.
✅Pros:
-
Control total sobre el comportamiento de las dependencias.
-
Rápido y ligero en términos de recursos.
-
Facilita la prueba de casos límite y situaciones difíciles de reproducir.
-
No genera colisiones con otros usuarios.
❌Contras:
-
Si nuestro código no está preparado para la inyección de dependencias, puede ser complicado de implementar.
-
Si la respuesta necesita de la interacción de varios controladores, la programación de todos los mocks puede resultar laboriosa.
-
Menos realista que otras opciones.
-
Riesgo de divergencia entre el mock y el comportamiento real.
-
Puede requerir un mantenimiento constante para mantener la sincronización con los cambios en las dependencias reales.
Código en proveedor
Si, por ejemplo, utilizamos Spring Boot, podemos declarar el Mock e inyectarlo en el controlador antes de comenzar cada prueba. De esta manera, podríamos definir el comportamiento y datos específicos para la prueba de forma rápida, sencilla y estanca.
class ProviderContractTest {
@Mock
private StudentRepository studentRepository;
@InjectMocks
private StudentController studentController;
@BeforeEach
void setUp(PactVerificationContext context) {
val testTarget \= new MockMvcTestTarget();
testTarget.setControllers(studentController);
context.setTarget(testTarget);
}
@State("Student 1 exists")
public void student1Exists() {
Student expected \= new Student();
expected.setId("6");
expected.setName("Sara");
expected.setEmail("fran.test@sngular.com");
expected.setStudentNumber(99);
given(studentRepository.findById("1"))
.willReturn(java.util.Optional.of(expected));
}
Conclusión
Si nuestro código lo permite, esta sería la opción ideal puesto que es la que nos da mayor control en la prueba, genera menos acoplamiento de datos y se ejecuta más rápidamente. Resulta especialmente útil para validar casos límite.
Cerrando
La gestión efectiva del estado del sistema es fundamental para el éxito del contract testing. Cada técnica tiene sus propias fortalezas y debilidades, y la elección dependerá de factores como la complejidad del sistema, número de dependencias, calidad del código, los recursos disponibles y los objetivos específicos de las pruebas.
En la práctica, muchos equipos optan por un enfoque híbrido, combinando diferentes técnicas según las necesidades de cada componente o servicio. Lo importante es mantener un equilibrio entre la reproducibilidad, la eficiencia de las pruebas y el coste de mantenimiento.
Independientemente de la técnica elegida, es crucial documentar y automatizar la configuración del estado del sistema. Esto no solo facilita la ejecución consistente de las pruebas, sino que también ayuda a los nuevos miembros del equipo a comprender y contribuir al proceso de testing.
En última instancia, una gestión adecuada del estado del sistema en contract testing conduce a pruebas más confiables, una mayor confianza en la integración entre servicios y, por ende, a sistemas más robustos y de mayor calidad.
Nuestras últimas novedades
¿Te interesa saber cómo nos adaptamos constantemente a la nueva frontera digital?
21 de noviembre de 2024
Contract Testing as a Service: apoya a tus clientes
14 de octubre de 2024
PactFlow & Contract Testing: Un caso de uso empresarial
7 de octubre de 2024
Configuración de Tareas Personalizadas de Lint en Gradle con Kotlin DSL
2 de octubre de 2024
Telice reduce el tiempo de gestión de pedidos de 21 días a 24 horas con Power Platform