paint-brush
Creación de pruebas de integración efectivas: mejores prácticas y herramientas dentro del marco Springpor@avvero
483 lecturas
483 lecturas

Creación de pruebas de integración efectivas: mejores prácticas y herramientas dentro del marco Spring

por Anton Belyaev8m2024/05/26
Read on Terminal Reader

Demasiado Largo; Para Leer

Este artículo ofrece recomendaciones prácticas para escribir pruebas de integración, demostrando cómo centrarse en las especificaciones de las interacciones con servicios externos, haciendo que las pruebas sean más legibles y fáciles de mantener. El enfoque no sólo mejora la eficiencia de las pruebas sino que también promueve una mejor comprensión de los procesos de integración dentro de la aplicación. A través de la lente de ejemplos específicos, se explorarán varias estrategias y herramientas, como contenedores DSL, JsonAssert y Pact, ofreciendo al lector una guía completa para mejorar la calidad y la visibilidad de las pruebas de integración.
featured image - Creación de pruebas de integración efectivas: mejores prácticas y herramientas dentro del marco Spring
Anton Belyaev HackerNoon profile picture
0-item

En el desarrollo de software moderno, las pruebas efectivas desempeñan un papel clave para garantizar la confiabilidad y estabilidad de las aplicaciones.


Este artículo ofrece recomendaciones prácticas para escribir pruebas de integración, demostrando cómo centrarse en las especificaciones de las interacciones con servicios externos, haciendo que las pruebas sean más legibles y fáciles de mantener. El enfoque no sólo mejora la eficiencia de las pruebas sino que también promueve una mejor comprensión de los procesos de integración dentro de la aplicación. A través de la lente de ejemplos específicos, se explorarán varias estrategias y herramientas, como contenedores DSL, JsonAssert y Pact, ofreciendo al lector una guía completa para mejorar la calidad y la visibilidad de las pruebas de integración.


El artículo presenta ejemplos de pruebas de integración realizadas utilizando Spock Framework en Groovy para probar interacciones HTTP en aplicaciones Spring. Al mismo tiempo, las principales técnicas y enfoques sugeridos se pueden aplicar eficazmente a varios tipos de interacciones más allá de HTTP.

Descripción del problema

El artículo Redacción de pruebas de integración efectivas en primavera: estrategias de prueba organizadas para simulación de solicitudes HTTP describe un enfoque para escribir pruebas con una clara separación en distintas etapas, cada una de las cuales desempeña su función específica. Describamos un ejemplo de prueba de acuerdo con estas recomendaciones, pero burlándonos no de una sino de dos solicitudes. La etapa Actuar (Ejecución) se omitirá por brevedad (puede encontrar un ejemplo de prueba completo en el repositorio del proyecto ).

El código presentado se divide condicionalmente en partes: "Código de soporte" (coloreado en gris) y "Especificación de interacciones externas" (coloreado en azul). El Código de soporte incluye mecanismos y utilidades para realizar pruebas, incluida la interceptación de solicitudes y la emulación de respuestas. La Especificación de interacciones externas describe datos específicos sobre servicios externos con los que el sistema debe interactuar durante la prueba, incluidas las solicitudes y respuestas esperadas. El Código de soporte sienta las bases para las pruebas, mientras que la Especificación se relaciona directamente con la lógica empresarial y las funciones principales del sistema que intentamos probar.


La Especificación ocupa una parte menor del código pero representa un valor significativo para comprender la prueba, mientras que el Código de Soporte, que ocupa una parte mayor, presenta menos valor y es repetitivo para cada declaración simulada. El código está diseñado para usarse con MockRestServiceServer. Con referencia al ejemplo de WireMock , se puede ver el mismo patrón: la especificación es casi idéntica y el código de soporte varía.


El objetivo de este artículo es ofrecer recomendaciones prácticas para redactar pruebas de tal manera que la atención se centre en la especificación y el código de soporte pase a un segundo plano.

Escenario de demostración

Para nuestro escenario de prueba, propongo un bot hipotético de Telegram que reenvía solicitudes a la API de OpenAI y envía respuestas a los usuarios.

Los contratos de interacción con los servicios se describen de forma simplificada para resaltar la lógica principal de la operación. A continuación se muestra un diagrama de secuencia que demuestra la arquitectura de la aplicación. Entiendo que el diseño puede generar preguntas desde la perspectiva de la arquitectura de sistemas, pero aborde esto con comprensión: el objetivo principal aquí es demostrar un enfoque para mejorar la visibilidad en las pruebas.

Propuesta

Este artículo analiza las siguientes recomendaciones prácticas para redactar exámenes:

  • Uso de contenedores DSL para trabajar con simulacros.
  • Uso de JsonAssert para verificación de resultados.
  • Almacenar las especificaciones de interacciones externas en archivos JSON.
  • Uso de archivos Pact.

Uso de envoltorios DSL para burlarse

El uso de un contenedor DSL permite ocultar el código simulado repetitivo y proporciona una interfaz sencilla para trabajar con la especificación. Es importante enfatizar que lo que se propone no es un DSL específico sino un enfoque general que implementa. A continuación se presenta un ejemplo de prueba corregido utilizando DSL ( texto de prueba completo ).

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess("{...}")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Donde el método restExpectation.openai.completions , por ejemplo, se describe de la siguiente manera:

 public interface OpenaiMock { /** * This method configures the mock request to the following URL: {@code https://api.openai.com/v1/chat/completions} */ RequestCaptor completions(DefaultResponseCreator responseCreator); }

Tener un comentario sobre el método permite, al pasar el cursor sobre el nombre del método en el editor de código, obtener ayuda, incluida la visualización de la URL de la que se burlará.

En la implementación propuesta, la declaración de la respuesta del simulacro se realiza utilizando instancias ResponseCreator , permitiendo instancias personalizadas, como por ejemplo:

 public static ResponseCreator withResourceAccessException() { return (request) -> { throw new ResourceAccessException("Error"); }; }

A continuación se muestra una prueba de ejemplo para escenarios fallidos que especifican un conjunto de respuestas:

 import static org.springframework.http.HttpStatus.FORBIDDEN setup: def openaiRequestCaptor = restExpectation.openai.completions(openaiResponse) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 0 where: openaiResponse | _ withResourceAccessException() | _ withStatus(FORBIDDEN) | _

Para WireMock, todo es igual, excepto que la formación de la respuesta es ligeramente diferente ( código de prueba , código de clase de fábrica de respuesta ).

Uso de la anotación @Language("JSON") para una mejor integración IDE

Al implementar un DSL, es posible anotar parámetros de método con @Language("JSON") para habilitar la compatibilidad con funciones de idioma para fragmentos de código específicos en IntelliJ IDEA. Con JSON, por ejemplo, el editor tratará el parámetro de cadena como código JSON, habilitando funciones como resaltado de sintaxis, autocompletado, verificación de errores, navegación y búsqueda de estructuras. A continuación se muestra un ejemplo del uso de la anotación:

 public static DefaultResponseCreator withSuccess(@Language("JSON") String body) { return MockRestResponseCreators.withSuccess(body, APPLICATION_JSON); }

Así es como se ve en el editor:

Uso de JsonAssert para la verificación de resultados

La biblioteca JSONAssert está diseñada para simplificar las pruebas de estructuras JSON. Permite a los desarrolladores comparar fácilmente cadenas JSON esperadas y reales con un alto grado de flexibilidad y admite varios modos de comparación.

Esto permite pasar de una descripción de verificación como esta

 openaiRequestCaptor.body.model == "gpt-3.5-turbo" openaiRequestCaptor.body.messages.size() == 1 openaiRequestCaptor.body.messages[0].role == "user" openaiRequestCaptor.body.messages[0].content == "Hello!"

a algo como esto

 assertEquals("""{ "model": "gpt-3.5-turbo", "messages": [{ "role": "user", "content": "Hello!" }] }""", openaiRequestCaptor.bodyString, false)

En mi opinión, la principal ventaja del segundo enfoque es que garantiza la coherencia de la representación de datos en varios contextos: en documentación, registros y pruebas. Esto simplifica significativamente el proceso de prueba, proporcionando flexibilidad en la comparación y precisión en el diagnóstico de errores. Por lo tanto, no solo ahorramos tiempo en la redacción y mantenimiento de pruebas, sino que también mejoramos su legibilidad e información.

Cuando se trabaja con Spring Boot, a partir de al menos la versión 2, no se necesitan dependencias adicionales para trabajar con la biblioteca, ya que org.springframework.boot:spring-boot-starter-test ya incluye una dependencia en org.skyscreamer:jsonassert .

Almacenamiento de la especificación de interacciones externas en archivos JSON

Una observación que podemos hacer es que las cadenas JSON ocupan una parte importante de la prueba. ¿Deberían estar ocultos? Si y no. Es importante entender qué trae más beneficios. Ocultarlos hace que las pruebas sean más compactas y simplifica la comprensión de la esencia de la prueba a primera vista. Por otro lado, para un análisis exhaustivo, parte de la información crucial sobre la especificación de la interacción externa estará oculta, lo que requerirá saltos adicionales entre archivos. La decisión depende de la conveniencia: haz lo que te resulte más cómodo.

Si elige almacenar cadenas JSON en archivos, una opción sencilla es mantener las respuestas y solicitudes por separado en archivos JSON. A continuación se muestra un código de prueba ( versión completa ) que demuestra una opción de implementación:

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json"))) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

El método fromFile simplemente lee una cadena de un archivo en el directorio src/test/resources y no incluye ninguna idea revolucionaria, pero aún está disponible en el repositorio del proyecto como referencia.

Para la parte variable de la cadena, se sugiere utilizar la sustitución con org.apache.commons.text.StringSubstitutor y pasar un conjunto de valores al describir el simulacro, por ejemplo:

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json", [content: "Hello! How can I assist you today?"])))

Donde la parte con sustitución en el archivo JSON se ve así:

 ... "message": { "role": "assistant", "content": "${content:-Hello there, how may I assist you today?}" }, ...

El único desafío para los desarrolladores al adoptar el enfoque de almacenamiento de archivos es desarrollar un esquema de ubicación de archivos adecuado en los recursos de prueba y un esquema de nombres. Es fácil cometer un error que puede empeorar la experiencia de trabajar con estos archivos. Una solución a este problema podría ser utilizar especificaciones, como las de Pact, que se analizarán más adelante.

Al utilizar el enfoque descrito en pruebas escritas en Groovy, es posible que encuentre inconvenientes: no hay soporte en IntelliJ IDEA para navegar al archivo desde el código, pero se espera que se agregue soporte para esta funcionalidad en el futuro . En pruebas escritas en Java, esto funciona muy bien.

Uso de archivos de contrato de Pact

Comencemos con la terminología.


La prueba de contrato es un método para probar puntos de integración donde cada aplicación se prueba de forma aislada para confirmar que los mensajes que envía o recibe se ajustan a un entendimiento mutuo documentado en un "contrato". Este enfoque garantiza que las interacciones entre las diferentes partes del sistema cumplan con las expectativas.


Un contrato en el contexto de las pruebas de contrato es un documento o especificación que registra un acuerdo sobre el formato y la estructura de los mensajes (solicitudes y respuestas) intercambiados entre aplicaciones. Sirve como base para verificar que cada aplicación pueda procesar correctamente los datos enviados y recibidos por otras en la integración.


El contrato se establece entre un consumidor (por ejemplo, un cliente que desea recuperar algunos datos) y un proveedor (por ejemplo, una API en un servidor que proporciona los datos que necesita el cliente).


Las pruebas impulsadas por el consumidor son un enfoque para las pruebas de contratos en el que los consumidores generan contratos durante sus ejecuciones de pruebas automatizadas. Estos contratos se pasan al proveedor, quien luego ejecuta su conjunto de pruebas automatizadas. Cada solicitud contenida en el archivo del contrato se envía al proveedor y la respuesta recibida se compara con la respuesta esperada especificada en el archivo del contrato. Si ambas respuestas coinciden, significa que el consumidor y el proveedor de servicios son compatibles.


Finalmente, Pacto. Pact es una herramienta que implementa las ideas de las pruebas de contratos impulsadas por el consumidor. Admite pruebas tanto de integraciones HTTP como de integraciones basadas en mensajes, centrándose en el desarrollo de pruebas de código primero.

Como mencioné anteriormente, podemos utilizar las especificaciones del contrato y las herramientas de Pact para nuestra tarea. La implementación podría verse así ( código de prueba completo ):

 setup: def openaiRequestCaptor = restExpectation.openai.completions(fromContract("openai/SuccessfulCompletion-Hello.json")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

El expediente del contrato está disponible para su revisión .

La ventaja de utilizar archivos de contrato es que contienen no solo el cuerpo de la solicitud y la respuesta, sino también otros elementos de la especificación de interacciones externas: ruta de la solicitud, encabezados y estado de la respuesta HTTP, lo que permite describir completamente un simulacro basado en dicho contrato.

Es importante tener en cuenta que, en este caso, nos limitamos a las pruebas por contrato y no nos extendemos a las pruebas impulsadas por el consumidor. Sin embargo, es posible que alguien quiera explorar Pact más a fondo.

Conclusión

Este artículo revisó recomendaciones prácticas para mejorar la visibilidad y la eficiencia de las pruebas de integración en el contexto del desarrollo con Spring Framework. Mi objetivo era centrarme en la importancia de definir claramente las especificaciones de las interacciones externas y minimizar el código repetitivo. Para lograr este objetivo, sugerí usar contenedores DSL y JsonAssert, almacenar especificaciones en archivos JSON y trabajar con contratos a través de Pact. Los enfoques descritos en el artículo tienen como objetivo simplificar el proceso de redacción y mantenimiento de pruebas, mejorar su legibilidad y, lo más importante, mejorar la calidad de las pruebas en sí al reflejar con precisión las interacciones entre los componentes del sistema.


Enlace al repositorio del proyecto que demuestra las pruebas: sandbox/bot .


¡Gracias por su atención al artículo y buena suerte en su búsqueda de escribir pruebas efectivas y visibles!