En proyectos relacionados con el desarrollo de arquitecturas de microservicios, CI/CD pasa de ser una opción agradable a convertirse en una necesidad urgente. Las pruebas automáticas son una parte integral de la integración continua, un enfoque adecuado del cual puede regalar al equipo muchas agradables noches con la familia y amigos. De lo contrario, el proyecto corre el riesgo de nunca completarse.
Se puede cubrir todo el código del microservicio con pruebas unitarias y objetos simulados, pero esto solo resuelve parcialmente el problema y deja muchas preguntas y complicaciones, especialmente al probar el manejo de datos. Como siempre, los más críticos son: la prueba de consistencia de datos en una base de datos relacional, la prueba del trabajo con servicios en la nube y las suposiciones incorrectas al escribir objetos simulados.
Todo esto, y un poco más, se resuelve probando todo el microservicio en un contenedor Docker. Una ventaja indiscutible para asegurar la validez de las pruebas es que las mismas imágenes de Docker que se llevan a producción son las que se someten a pruebas.
La automatización de tal enfoque presenta una serie de problemas, cuya solución se describirá a continuación:
- conflictos de tareas paralelas en un solo host Docker;
- conflictos de identificadores en la base de datos durante las iteraciones de la prueba;
- espera de la disponibilidad de los microservicios;
- unificación y salida de registros a sistemas externos;
- pruebas de solicitudes HTTP salientes;
- pruebas de websockets (usando SignalR);
- pruebas de autenticación y autorización OAuth.
Este es un artículo basado en en SECR 2019. Así que para aquellos que no quieren leer, .

En este artículo, explicaré cómo lanzar un servicio de prueba en Docker, así como la base de datos y los servicios de Amazon AWS mediante un script, luego realizar pruebas en Postman y, tras su finalización, detener y eliminar los contenedores creados. Las pruebas se realizan con cada cambio en el código. De esta manera, nos aseguramos de que cada versión funcione correctamente con la base de datos y los servicios de AWS.
El mismo script es ejecutado tanto por los desarrolladores en sus escritorios Windows como por el servidor Gitlab CI en Linux.
Para que la implementación de nuevas pruebas esté justificada, no debe requerir la instalación de herramientas adicionales ni en la computadora del desarrollador ni en el servidor donde se ejecutan las pruebas al realizar un commit. Docker resuelve esta tarea.
La prueba debe ejecutarse en un servidor local por las siguientes razones:
- La red nunca es completamente confiable. De mil solicitudes, una puede fallar;
En tal caso, la prueba automática fallará, el trabajo se detendrá y será necesario buscar la causa en los registros; - Algunos servicios externos no permiten solicitudes demasiado frecuentes.
Además, utilizar un entorno de prueba no es recomendable porque:
- No solo el código malo que se ejecuta en el entorno puede romperlo, sino también los datos que el código correcto no puede procesar;
- Por más que intentemos revertir todos los cambios realizados por la prueba, durante la propia prueba, algo puede salir mal (de lo contrario, ¿para qué probar?).
Sobre el proyecto y la organización del proceso
Nuestra empresa desarrolló una aplicación web microservicios que funciona en Docker en la nube de Amazon AWS. En el proyecto ya se utilizaban pruebas unitarias, sin embargo, a menudo surgían errores que las pruebas unitarias no detectaban. Era necesario probar todo un microservicio junto con la base de datos y los servicios de Amazon.
En el proyecto se aplica un proceso estándar de integración continua, que incluye la prueba del microservicio en cada commit. Después de asignar la tarea, el desarrollador realiza cambios en el microservicio, lo prueba manualmente y ejecuta todas las pruebas automáticas disponibles. Si es necesario, el desarrollador modifica las pruebas. Si no se detectan problemas, se realiza un commit en la rama de la tarea correspondiente. Después de cada commit, se ejecutan automáticamente las pruebas en el servidor. La fusión en la rama principal y la ejecución de pruebas automáticas en ella se llevan a cabo tras una revisión exitosa. Si las pruebas en la rama principal pasan, el servicio se actualiza automáticamente en el entorno de prueba en Amazon Elastic Container Service (el entorno de prueba). Este entorno es necesario para todos los desarrolladores y testers, y no debe ser dañado. Los testers en este entorno verifican la solución o nueva característica realizando pruebas manuales.
Arquitectura del proyecto

La aplicación consta de más de diez servicios. Algunos de ellos están escritos en .NET Core y otros en NodeJs. Cada servicio funciona en un contenedor Docker en Amazon Elastic Container Service. Cada uno tiene su propia base de datos Postgres, y algunos también tienen Redis. No hay bases de datos compartidas. Si varios servicios necesitan los mismos datos, estos se envían a cada uno de ellos a través de SNS (Simple Notification Service) y SQS (Amazon Simple Queue Service) en el momento de su modificación, y los servicios los almacenan en sus bases de datos separadas.
SQS y SNS
SQS permite enviar mensajes a la cola y leer mensajes de la cola a través del protocolo HTTPS.
Si varios servicios leen de una misma cola, cada mensaje llega solo a uno de ellos. Esto es útil cuando se lanzan múltiples instancias de un mismo servicio para distribuir la carga entre ellas.
Si es necesario que cada mensaje se entregue a varios servicios, cada destinatario debe tener su propia cola, y para duplicar mensajes en varias colas se necesita SNS.
En SNS se crea un tema y se suscribe, por ejemplo, a una cola SQS. Se pueden enviar mensajes al tema. En este caso, el mensaje se envía a cada cola suscrita a este tema. En SNS no hay método para leer mensajes. Si durante la depuración o las pruebas es necesario averiguar qué se envía a SNS, se puede crear una cola SQS, suscribirla al tema correspondiente y leer la cola.

API Gateway
La mayoría de los servicios no están disponibles directamente desde Internet. El acceso se realiza a través de API Gateway, que verifica los derechos de acceso. Este también es nuestro servicio, y también tiene pruebas.
Notificaciones en tiempo real
La aplicación utiliza , para mostrar al usuario notificaciones en tiempo real. Esto se implementa en el servicio de notificaciones. Este es accesible directamente desde Internet y trabaja con OAuth, porque integrar el soporte de Websockets en Gateway resultó poco práctico en comparación con la integración de OAuth y el servicio de notificaciones.
Un enfoque conocido para las pruebas
Las pruebas unitarias reemplazan elementos como bases de datos con objetos simulados. Si un microservicio, por ejemplo, intenta crear una entrada en una tabla con una clave externa, y la entrada a la que se refiere esa clave no existe, la solicitud no puede ser ejecutada. Las pruebas unitarias no pueden detectar esto.
En se propone utilizar una base de datos en memoria e introducir objetos simulados.
La base en memoria es uno de los sistemas de gestión de bases de datos que soporta Entity Framework. Fue creada específicamente para pruebas. Los datos en esta base se almacenan solo hasta que finaliza el proceso que la utiliza. No es necesario crear tablas, y la integridad de los datos no se verifica.
Los objetos mock simulan la clase sustituible solo en la medida en que el desarrollador de la prueba comprende su funcionamiento.
El artículo de Microsoft no indica cómo lograr el inicio automático de Postgres y la ejecución de migraciones al iniciar la prueba. Mi solución hace esto y, además, no se añade ningún código específicamente para pruebas al microservicio.
Pasemos a la solución
Durante el proceso de desarrollo, se hizo evidente que las pruebas unitarias eran insuficientes para detectar todos los problemas a tiempo, por lo que se decidió abordar esta cuestión desde otro ángulo.
Configuración del entorno de prueba
La primera tarea es desplegar el entorno de prueba. Los pasos necesarios para iniciar el microservicio son:
- Configurar el servicio que se está probando en el entorno local, especificando las credenciales para la conexión a la base de datos y AWS en las variables de entorno;
- Iniciar Postgres y ejecutar la migración iniciando Liquibase.
En los sistemas de gestión de bases de datos relacionales, antes de escribir datos en la base, es necesario crear el esquema de datos, en términos simples, las tablas. Al actualizar la aplicación, las tablas deben ajustarse a la forma utilizada por la nueva versión, preferiblemente sin pérdida de datos. Esto se llama migración. La creación de tablas en una base inicialmente vacía es un caso particular de migración. La migración se puede integrar en la propia aplicación. Tanto en .NET como en NodeJS hay frameworks para migraciones. En nuestro caso, por razones de seguridad, los microservicios no tienen el derecho de modificar el esquema de datos, y la migración se realiza mediante Liquibase. - Iniciar Amazon LocalStack. Esta es una implementación de los servicios de AWS para ejecutarlos localmente. Para LocalStack, hay una imagen lista en Docker Hub.
- Ejecutar un script para crear las entidades necesarias en LocalStack. Los scripts de shell utilizan AWS CLI.
Para las pruebas del proyecto se utiliza . Ya existía antes, pero se ejecutaba manualmente y se probaba la aplicación una vez desplegada en el entorno. Esta herramienta permite realizar solicitudes HTTP(S) arbitrarias y verificar que las respuestas coincidan con las expectativas. Las solicitudes se agrupan en una colección y se puede ejecutar toda la colección de una vez.

Cómo funciona la prueba automática
Durante la prueba, todo funciona en Docker: tanto el servicio que se está probando, como Postgres, la herramienta de migración y Postman, o más bien, su versión de consola: Newman.
Docker resuelve una serie de problemas:
- Independencia de la configuración del host;
- Instalación de dependencias: Docker descarga imágenes desde Docker Hub;
- Devolución del sistema a su estado original: simplemente eliminamos los contenedores.
Docker-compose une los contenedores en una red virtual aislada de Internet, donde los contenedores se encuentran entre sí por nombre de dominio.
La prueba es gestionada por un script de shell. Para ejecutar la prueba en Windows, usamos git-bash. Así, un solo script es suficiente tanto para Windows como para Linux. Git y Docker están instalados en todos los desarrolladores del proyecto. Al instalar Git en Windows, se instala git-bash, por lo que todos también lo tienen.
El script ejecuta los siguientes pasos:
- Construcción de imágenes de Docker
docker-compose build - Inicio de la base de datos y LocalStack
docker-compose up -d - Migración de la base de datos y preparación de LocalStack
docker-compose run - Inicio del servicio en prueba
docker-compose up -d - Ejecución de la prueba (Newman)
- Detención de todos los contenedores
docker-compose down - Publicar resultados en Slack
Tenemos un chat, donde llegan los mensajes con una marca verde o una cruz roja y un enlace al registro.
En estos pasos, se utilizan las siguientes imágenes de Docker:
- El servicio en prueba es la misma imagen que la de producción. La configuración para la prueba se realiza a través de variables de entorno.
- Para Postgres, Redis y LocalStack se utilizan imágenes listas de Docker Hub. También hay imágenes listas para Liquibase y Newman. Nosotros construimos nuestras propias imágenes sobre sus bases, añadiendo nuestros archivos.
- Para preparar LocalStack se utiliza una imagen predefinida de AWS CLI, sobre la cual se crea una imagen que contiene el script.
virtualización puede ejecutar máquinas virtuales que funcionan con , no es necesario construir una imagen de Docker solo para agregar archivos al contenedor. Sin embargo, los volúmenes no son adecuados para nuestro entorno, ya que las tareas de Gitlab CI se ejecutan en contenedores. Desde dicho contenedor se puede gestionar Docker, pero los volúmenes montan carpetas solo desde el sistema host, y no desde otro contenedor.
Problemas que se pueden encontrar
Esperar que esté listo
Cuando el contenedor con el servicio está en marcha, aún no significa que esté listo para aceptar conexiones. Hay que esperar a que se establezca la conexión para continuar.
A veces, esta tarea se resuelve con un script , que espera la oportunidad de establecer una conexión TCP. Sin embargo, LocalStack puede devolver un error 502 Bad Gateway. Además, se compone de múltiples servicios, y si uno de ellos está listo, no dice nada sobre los demás.
Solución: scripts de preparación de LocalStack, que esperan una respuesta 200 tanto de SQS como de SNS.
Conflictos de tareas paralelas
Varios tests pueden ejecutarse simultáneamente en un mismo host de Docker, por lo que los nombres de los contenedores y redes deben ser únicos. Además, los tests de diferentes ramas de un mismo servicio también pueden ejecutarse al mismo tiempo, por lo que no es suficiente escribir nombres únicos en cada archivo de compose.
Solución: el script establece un valor único para la variable COMPOSE_PROJECT_NAME.
Particularidades de Windows
Al usar Docker en Windows hay varias cosas a las que quiero llamar su atención, ya que esta experiencia es importante para entender las razones de los errores.
- Los scripts de shell en el contenedor deben tener finales de línea de Linux.
El símbolo CR para el shell es un error sintáctico. Por el mensaje de error es difícil entender que es por esto. Al editar tales scripts en Windows, se necesita un editor de texto adecuado. Además, el sistema de control de versiones debe estar configurado correctamente.
Así es como se configura git:
git config core.autocrlf input- Git-bash emula las carpetas estándar de Linux y, al llamar a un archivo exe (incluido docker.exe), convierte rutas absolutas de Linux en rutas de Windows. Sin embargo, esto no tiene sentido para rutas que no están en la máquina local (o rutas dentro del contenedor). Este comportamiento no se puede desactivar.
Solución: agregar una barra adicional al inicio de la ruta: \/\/bin en lugar de \/bin. Linux entiende estas rutas; para él, varias barras son lo mismo que una. Pero git-bash no reconoce estas rutas y no intenta transformarlas.
Salida de logs
Al ejecutar pruebas, me gustaría ver los logs tanto de Newman como del servicio bajo prueba. Dado que los eventos de estos logs están relacionados, combinarlos en una sola consola es mucho más conveniente que tener dos archivos separados. Newman se inicia a través de docker-compose run, por lo que su salida va a la consola. Queda por hacer que también la salida del servicio llegue allí.
La solución inicial consistía en hacer docker-compose up sin la bandera -d, pero, utilizando las capacidades del shell, enviar este proceso al fondo:
docker-compose up <service> &Esto funcionaba hasta que fue necesario enviar logs de docker a un servicio externo. docker-compose up dejó de mostrar los registros en la consola. Sin embargo, el comando funcionó docker attach.
Solución:
docker attach --no-stdin ${COMPOSE_PROJECT_NAME}__1 &Conflicto de identificadores durante las iteraciones de la prueba
Las pruebas se ejecutan en varias iteraciones. La base de datos no se limpia. Las entradas en la base de datos tienen ID únicos. Si se escriben ID específicos en las consultas, en la segunda iteración se producirá un conflicto.
Para evitarlo, los ID deben ser únicos, o se deben eliminar todos los objetos creados por la prueba. No se pueden eliminar algunos objetos, de acuerdo con los requisitos.
Solución: generar GUIDs mediante scripts en Postman.
var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);Luego en la consulta usar el símbolo {{myUUID}}, que será reemplazado por el valor de la variable.
Interacción a través de LocalStack
Si el servicio que se prueba lee de la cola SQS o escribe en ella, entonces para verificar esto, la prueba también debe trabajar con esta cola.
Solución: consultas de Postman a LocalStack.
La API de los servicios de AWS está documentada, lo que permite hacer consultas sin SDK.
Si el servicio escribe en la cola, la leemos y verificamos el contenido del mensaje.
Si el servicio envía mensajes a SNS, en la etapa de preparación se crea también una cola y se suscribe a este tema SNS. Todo se reduce a lo descrito anteriormente.
Si el servicio debe leer un mensaje de la cola, en el paso anterior de la prueba escribimos este mensaje en la cola.
Pruebas de solicitudes HTTP originadas del microservicio que se prueba
Algunos servicios operan mediante HTTP con algo diferente a AWS, y algunas funciones de AWS no están implementadas en LocalStack.
Solución: en estos casos puede ayudar , que tiene una imagen lista en . Las solicitudes y respuestas esperadas se configuran mediante una solicitud HTTP. La API está documentada, así que hacemos solicitudes desde Postman.
Pruebas de autenticación y autorización OAuth
Utilizamos OAuth y . Para la prueba, necesitamos un proveedor de OAuth que podamos iniciar localmente.
Toda la interacción del servicio con el proveedor de OAuth se reduce a dos solicitudes: primero se solicita la configuración /.well-known/openid-configuration, y luego se solicita la clave pública (JWKS) en la dirección de la configuración. Todo esto es contenido estático.
Solución: nuestro proveedor de OAuth de prueba es un servidor de contenido estático y dos archivos en él. El token se generó una vez y se comprometió en Git.
Particularidades de las pruebas de SignalR
Postman no funciona con WebSockets. Se creó una herramienta especial para probar SignalR.
El cliente de SignalR no solo puede ser un navegador. Existe una biblioteca cliente para .NET Core. El cliente, escrito en .NET Core, establece una conexión, pasa la autenticación y espera una secuencia específica de mensajes. Si se recibe un mensaje inesperado o se interrumpe la conexión, el cliente finaliza con el código 1. Al recibir el último mensaje esperado, finaliza con el código 0.
Newman se ejecuta simultáneamente con el cliente. Se lanzan varios clientes para verificar que los mensajes se entregan a todos los destinatarios.

Se utiliza la opción para ejecutar varios clientes --scale en la línea de comandos de docker-compose.
Antes de que Postman inicie, el script espera a que todos los clientes establezcan una conexión.
Ya hemos encontrado un problema con la espera de la conexión. Pero allí eran servidores, y aquí es un cliente. Se necesita un enfoque diferente.
Solución: el cliente en el contenedor utiliza un mecanismo , para informar al script en el host sobre su estado. El cliente crea un archivo en una ruta específica, digamos, \/healthcheck, tan pronto como la conexión se establece. El script de HealthCheck en el Dockerfile es así:
HEALTHCHECK --interval=3s CMD if [ ! -e \/healthcheck ]; then false; fiComando docker inspect muestra para el contenedor el estado normal, el estado de salud y el código de finalización.
Después de que Newman finaliza, el script verifica que todos los contenedores con el cliente hayan terminado, y que sea con el código 0.
La felicidad existe
Después de superar las dificultades mencionadas anteriormente, tenemos un conjunto de pruebas que funcionan de manera estable. En las pruebas, cada servicio funciona como un todo, interactúa con la base de datos y con Amazon LocalStack.
Estas pruebas protegen a un equipo de más de 30 desarrolladores de errores en una aplicación con una interacción compleja de más de 10 microservicios durante los frecuentes despliegues.
Fuente: habr.com
