Contract testing vs testing funcional

Contract testing vs testing funcional

Francisco Moreno, QE Director

Francisco Moreno

QE Director

7 de mayo de 2024

Conoce la diferencia entre los diferentes tipos de pruebas y establece una estrategia de testing adecuada

En la actualidad, el desarrollo de software se caracteriza por la adopción de arquitecturas distribuidas, donde cada servicio se encarga de una funcionalidad específica y se comunica con otros a través de intercambio de mensajes de red, típicamente mediante peticiones REST. En este contexto, surge la necesidad de garantizar la compatibilidad entre los servicios, asegurando que interactúen de forma esperada y que los cambios en uno no afecten negativamente a los demás. Es aquí donde el Contract Testing, y en particular la herramienta Pact, juega un papel crucial.

Pero ¿qué diferencia al contract testing con las pruebas de integración y funcionales? Eso será lo que veremos en este artículo.

Por qué es interesante

Conocer la diferencia entre los diferentes tipos de pruebas te permitirá establecer una estrategia de testing adecuada, que optimice el proceso de desarrollo software.

Ser capaz de diferenciar el objetivo y, sobre todo, el alcance de cada tipo de prueba hará que tengamos una suite de testing mucho más fiable y rápida. Esto es así, puesto que será más fácil detectar errores en fases tempranas, ser mucho más precisos a la hora de identificar el origen del problema, evitar bloqueos entre equipos y mejorar la comunicación entre los mismos.

Profundizando

A la hora de testear APIs, veamos ahora cuáles son las diferencias fundamentales entre los tipos de pruebas

Contract Testing Functional Testing End-to-End Testing
¿Qué es? Validan la interacción entre dos servicios según un "contrato" acordado. Verifican la integración y el correcto funcionamiento entre módulos o servicios. Pruebas que simulan escenarios de usuario completos desde principio a fin.
Objetivo del test Asegurar que los servicios se pueden comunicar correctamente según los contratos definidos. Verificar que diferentes módulos o servicios funcionan juntos como se espera. Validar el sistema completo, incluyendo integraciones con terceros.
¿Cómo funciona? Utiliza herramientas como Pact para simular las llamadas entre servicios, con ejemplos concretos para verificar que se cumplan los contratos. Generalmente, requiere que todos los componentes estén operativos para probar su interacción. Implica pruebas completas del sistema, simulando workflows de un usuario final.
Alcance del test Se enfoca en la interacción específica entre servicios basada en contratos definidos. Cubre múltiples componentes o servicios, pero no necesariamente el sistema completo. Involucra todo el sistema y todas sus dependencias operativas.
Etapa del SDLC Durante el desarrollo,antes de la integración completa. Después del desarrollo de módulos individuales, antes de la entrega final. Generalmente en las etapas finales del ciclo de desarrollo o durante la aceptación del usuario.
Beneficios del test Rápido y eficiente para detectar problemas de integración en fases tempranas. Ayuda a identificar problemas y casos límite en los sistemas. Asegura que el sistema completo funcione correctamente para el usuario final.
Limitaciones No verifica la funcionalidad completa ni el rendimiento del sistema. No captura errores que podrían aparecer solo en la interacción completa de todos los componentes. Requiere más tiempo y recursos; puede ser complicado automatizar completamente.

Una vez vistas la diferencias, y teniendo claro que el contract testing se basa en la compatibilidad entre servicios, veamos en qué escenarios es interesante aplicarlo y cuándo no.

✅ Validar que el cuerpo de los mensajes es adecuado

Esta es una de las principales ventajas del uso de contract testing, ya que, mediante la especificación de ejemplos concretos (“expectations”) y el mockeo de peticiones de red, podemos comprobar, de manera automática, que tanto productor como consumidor intercambian información de la manera acordada.

Esto no es simplemente comprobar el esquema de datos, las principales diferencias son:

  • Sólo se tienen en cuenta los campos que realmente afectan a la parte consumidora, pudiendo obviar el resto de datos de la respuesta.

  • Se permite el uso de “wildcards” o expresiones regulares para validar el tipo de dato esperado (email, fecha, número, string, etc.). Esto evita fragilidad en las comprobaciones puesto que no queda atado al valor concreto.

  • No es necesario tener la parte productora desplegada, las validaciones se hacen sobre la base del contrato definido y mediante aplicaciones mockeadas que generar peticiones reales de red.

✅ Validar que el cliente será capaz de procesar la respuesta acordada

Para no entrar en el terreno de las pruebas funcionales, debemos comprender que el ámbito de esta comprobación es, únicamente, validar que el consumidor es capaz de procesar la respuesta del proveedor de manera adecuada. No tratamos de comprobar los efectos laterales que se desencadenan posteriormente.

Por ejemplo, si nuestra aplicación cliente pide los datos de un alumno para calcular su nota media, la prueba en la parte de cliente irían encaminadas a que, efectivamente, se reciben los datos del alumno y que somos capaces de procesarlos de manera adecuada. En la práctica, la mayoría de los casos, consistirá en comprobar que tenemos una clase que mapea dicha respuesta de manera correcta.

Aquí podemos ver el código resultante

//Expectation

@Pact(consumer \= "student-consumer", provider \= "student-provider")

public V4Pact getStudentWithId1(PactDslWithProvider builder) {

    return builder.given("student 1 exists")

            .uponReceiving("get an existing student with ID 1")

            .path("/students/1")

            .method("GET")

            .willRespondWith()

            .status(200)

            .headers(Map.of("Content-Type", "application/json"))

            .body(newJsonBody(object \-> {

                object.stringType("id", "1");

                object.stringType("name", "Fake name");

                object.stringType("email", "some.email@sngular.com");

                object.numberType("studentNumber", 23);

            }).build())

            .toPact().asV4Pact().get();

}

//Validación

@Test

@PactTestFor(pactMethod \= "getStudentWithId1")

void getStudentWhenStudentExist(MockServer mockServer) {

    Student expected \= Student.builder()

            .id("1")

            .name("Fake name")

            .email("some.email@sngular.com")

            .studentNumber(23).build();

  

    RestTemplate restTemplate \= new RestTemplateBuilder().rootUri(mockServer.getUrl()).build();

    //Comprobamos que nuestro servicio es capaz de realizar y procear la respuesta adecuadamente

    Student student \= new StudentService(restTemplate).getStudent("1");

  

    assertEquals(expected, student);

}

✅ Validar que la ruta de los endpoints son correctos

Siguiendo el ejemplo de código anterior, podemos ver cómo estamos especificando la ruta y los parámetros concretos con los que debe hacerse la petición (”students/1”). Esto forzará a que ambas partes cumplan lo acordado en cuanto a la definición de endpoints.

✅ Comprobar que los códigos de respuesta son adecuados

También, en el ejemplo de código, podemos ver cómo se especifica el código de respuesta esperado (200). Como en caso anterior, nos estamos asegurando de que el proveedor enviará el código de respuesta acordado.

✅ Validar que las cabeceras y parámetros de la petición son adecuados

El ejemplo de código también podemos apreciar cómo se especifican las cabeceras de la petición esperadas. Por ello, en caso de no cumplirse por alguna de las dos partes, la prueba se daría por fallada.

❌ Validar diferentes casuísticas de peticiones

Supongamos que queremos validar que a hacer peticiones de tipo “POST”, el proveedor devuelve “400 Bad Request” ante determinados casos.

En este caso, como hemos mencionado anteriormente, mediante contract testing deberíamos comprobar que, efectivamente, existe la posibilidad de que el proveedor devuelva el código 400 y que el cliente es capaz de procesar ese tipo de respuesta.

Sin embargo, no sería adecuado comprobar mediante contract testing todas las posibilidades ante las que el servicio puede devolver “Bad Request”. De hacerlo, estaríamos haciendo que nuestras pruebas fuesen demasiado frágiles, ya que dependerían en gran medida de los ejemplos concretos que estemos utilizando para generar los concretos. Debemos tener en cuenta, que las reglas de validación o negocio, pueden cambiar y esto no debería afectar a la comunicación entre servicios.

Por ejemplo, esta no sería una buena definición de contrato, ya que si se decide cambiar la longitud máxima permitida, los test fallarían aun siendo ambos sistemas compatibles.

When "creating a user with a blank username"

  POST /users { "username": "", email: "...", ... }

Then

  Expected Response is 400 Bad Request

  Expected Response body is { "error": "username cannot be blank" }
When "creating a user with a username with 21 characters"

  POST /users { "username": "thisisalooongusername", email: "...", ... }

Then

  Expected Response is 400 Bad Request

  Expected Response body is { "error": "username cannot be more than 20 characters" }
When "creating a user with a username containing numbers"  

POST /users { "username": "us3rn4me", email: "...", ... }

Then  Expected Response is 400 Bad Request  

Expected Response body is { "error": "username can only contain letters" }

En este caso, con un único ejemplo de uso donde se especifique un “Bad Request” genérico sería suficiente. De esta manera, también deberíamos ser más genéricos a la hora de especificar el mensaje de vuelta esperado, evitando concretar el texto exacto esperado.

Finalmente, podría tener un caso así

When "creating a user with an invalid username"

  POST /users { "username": "bad\_username\_that\_breaks\_some\_rule\_that\_you\_are\_fairly\_confident\_will\_not\_change", ... }

Then

  Response is 400 Bad Request

  Response body is { "error": "<any string>" }

❌ Validar efectos laterales

Por efecto lateral de una prueba podemos entender cambios en el sistema tales como: cambios en base de datos, llamadas a servicios de terceros, modificaciones en archivos, envío de notificaciones, etc.

Aunque técnicamente se puedan verificar este tipo de situaciones utilizando Pact, no es el objetivo de las pruebas de contrato. Los principales inconvenientes son que hace que estos tests sean extremadamente frágiles y dependientes de otros sistemas.

Debido a ello, aun siendo posible, sería contraproducente, puesto que estaríamos las principales ventajas que ofrece Pact como son la rapidez de ejecución, independencia de las pruebas y el evitar bloqueos.

Por tanto, este tipo de validaciones deberían quedar pospuestas para siguientes fases: integración o e2e.

❌ Validar workflows (secuencia de pasos)

Pact permite especificar un estado inicial del sistema previo a la ejecución de la prueba. Este “setup” es esencial en cualquier framework de pruebas. No obstante, no debemos confundir esto, con una ejecutar una serie de acciones de manera secuencial y comprobar el estado final.

Es decir, no es lo mismo preparar el sistema para que existan un determinado usuario sobre el que pido datos, a que, sobre el mismo usuario, modifique un campo y luego compruebe las consecuencias que ha tenido dicho cambio. 

Por ejemplo, mediante contract testing no sería adecuado plantear una prueba de es estilo

Given a VIP user with ID 1 

When

  PUT /users/1 { VIP: "false" ... }

Then

  Expected Response is 200 OK

And

  GET /discounts?user=1

Then

  Expected Response is 200 OK

  Expected Response body {"No discounts for user 1"}

Resumiendo

En un mundo donde la arquitectura de microservicios y las aplicaciones distribuidas son la norma, la importancia de una estrategia de pruebas sólida y eficiente no puede subestimarse. En este artículo hemos explorado las distinciones clave entre Contract Testing, Integration Testing y End-to-End Testing, subrayando su relevancia y aplicabilidad en diferentes escenarios de desarrollo de software.

Contract Testing, facilitado por herramientas como Pact, se centra en asegurar que los servicios interactúen correctamente según los contratos definidos, ofreciendo una forma eficiente de detectar problemas de comunicación de manera temprana en el ciclo de desarrollo. Integration Testing verifica la cooperación entre módulos o servicios, identificando problemas que solo aparecen cuando estos componentes están desplegados e interactúan entre sí. Finalmente, End-to-End Testing evalúa el sistema completo en un entorno que simula la interacción del usuario final, garantizando que todas las partes del sistema funcionen en conjunto de manera fluida.

Al comprender estas diferencias y aplicar el tipo de prueba adecuado en el momento adecuado, los equipos pueden mejorar significativamente la eficiencia de su flujo de trabajo de desarrollo, detectar errores en fases tempranas y eliminar bloqueos, todo mientras se aseguran de que cada componente funcione según lo previsto.

Incorporar estas prácticas no solo optimiza el proceso de desarrollo, sino que también mejora la colaboración entre los equipos, asegurando que los cambios en un servicio no afecten a otros. Al final, la elección de la estrategia de testing adecuada es fundamental para el desarrollo de software de alta calidad.

Francisco Moreno, QE Director

Francisco Moreno

QE Director

Francisco Moreno, Director de QE en Sngular, lidera la estrategia de testing de la compañía y a un equipo de profesionales especializados en el aseguramiento de la calidad en proyectos ágiles.


Nuestras últimas novedades

¿Te interesa saber cómo nos adaptamos constantemente a la nueva frontera digital?

Gestión de la memoria en Swift
Gestión de la memoria en Swift

Tech Blog

10 de mayo de 2024

Gestión de la memoria en Swift

Accesibilidad en aplicaciones
Accesibilidad en aplicaciones

Tech Blog

7 de mayo de 2024

Accesibilidad en aplicaciones

Deja huella en las personas, no en el planeta
Deja huella en las personas, no en el planeta

Insight

16 de abril de 2024

Deja huella en las personas, no en el planeta

Adoptando el Desarrollo de Aplicaciones con Low Code y Mendix
Adoptando el Desarrollo de Aplicaciones con Low Code y Mendix

Tech Blog

20 de febrero de 2024

Adoptando el Desarrollo de Aplicaciones con Low Code y Mendix