Integración y pruebas del sistema completo

Lectura
30 min~11 min lectura

Integración y Pruebas del Sistema Completo de Microservicios

Llegamos al momento culminante de nuestro proyecto integrador: la integración y las pruebas del sistema completo. Hasta ahora, hemos construido microservicios aislados para gestionar productos, usuarios, órdenes y el carrito de compras. Cada uno funciona correctamente en su contenedor, pero un sistema de e-commerce es más que la suma de sus partes. En esta lección, aprenderás a orquestar la comunicación entre todos estos servicios, simular flujos de negocio completos (como un usuario que busca un producto, lo añade al carrito y completa una compra) y, lo más importante, a validar que todo el ecosistema funcione de manera robusta, resiliente y con el alto rendimiento que promete Go. Este proceso es análogo a ensamblar las piezas de un motor de alto rendimiento: cada pieza puede ser perfecta, pero solo cuando están correctamente conectadas, sincronizadas y lubricadas, el motor entrega toda su potencia.

La integración va más allá de conectar puntos finales (endpoints). Implica gestionar la propagación de errores entre servicios, garantizar la consistencia de datos a pesar de la naturaleza distribuida del sistema, y manejar escenarios de fallo parcial donde un servicio puede estar caído mientras los demás siguen operativos. Las pruebas en este contexto se vuelven multidimensionales: debemos probar no solo la funcionalidad, sino la latencia, la concurrencia y la recuperación ante fallos. Utilizaremos Docker Compose para levantar toda nuestra infraestructura de manera reproducible, y escribiremos scripts de prueba en Go que actúen como clientes simulando tráfico real, verificando cada paso del camino.

Concepto Clave: La Orquestación y el Contrato de Servicios

Imagina una orquesta sinfónica. Cada músico (microservicio) es un experto en su instrumento (su dominio: productos, usuarios, etc.). Pueden practicar sus partes a la perfección de forma aislada. Sin embargo, para que suene una sinfonía (el flujo de negocio completo), necesitan un director (la orquestación), una partitura común (los contratos de API) y la capacidad de escucharse entre sí (la comunicación). La orquestación es el proceso de coordinar y gestionar las interacciones entre estos servicios independientes para ejecutar una tarea mayor. En nuestro caso, Docker Compose será nuestro director de orquesta, definiendo qué servicios se levantan, en qué orden y cómo se conectan entre sí a través de la red.

El contrato de servicios es la partitura. Es la definición exacta de cómo se comunican los servicios: los endpoints HTTP, los formatos de los cuerpos de las solicitudes y respuestas (usualmente JSON), los códigos de estado esperados y los esquemas de los mensajes. En un sistema distribuido, acoplarse débilmente a través de contratos bien definidos es fundamental. Si el servicio de "Órdenes" espera que el servicio de "Usuarios" devuelva un campo customer_id, cualquier cambio en ese campo por parte del equipo de "Usuarios" debe ser comunicado y versionado para no romper el flujo de "Órdenes". Este contrato es lo que probamos durante la integración: verificamos que cada servicio cumple su promesa de interfaz hacia los demás.

Cómo Funciona en la Práctica: Flujo de una Compra Integrada

Vamos a desglosar, paso a paso, el flujo integrado más crítico en un e-commerce: la creación de una orden. Este flujo involucra a casi todos nuestros microservicios y pone a prueba nuestra arquitectura. Primero, un cliente autenticado (su token JWT ha sido validado por el API Gateway o el servicio de Usuarios) tiene items en su carrito. Al solicitar "checkout", el servicio de "Órdenes" recibe la petición. Su primera tarea es validar la sesión del usuario llamando al servicio de "Usuarios". Luego, debe recuperar los detalles del carrito del servicio de "Carrito". Con esa lista de productos, debe verificar el inventario y los precios actuales llamando al servicio de "Productos". Solo si todo está disponible y válido, procede a crear la orden en su base de datos, quizá enviando un evento a una cola para que el servicio de "Inventario" actualice los stocks y el servicio de "Notificaciones" envíe un correo de confirmación.

Para probar este flujo de manera automatizada, escribiremos un script que simule este cliente. El script: 1) Se autenticará contra el servicio de Usuarios para obtener un token. 2) Creará o usará un producto existente a través del servicio de Productos. 3) Añadirá ese producto al carrito del usuario. 4) Finalmente, disparará la solicitud de creación de orden al servicio de Órdenes. En cada paso, verificaremos los códigos de respuesta HTTP y la estructura de los datos devueltos. Además, monitorearemos los logs de cada contenedor para asegurarnos de que las llamadas entre servicios se están realizando correctamente y que no hay errores silenciosos. Este es el núcleo de una prueba de integración de extremo a extremo (end-to-end).

Código en Acción: Docker Compose y Script de Prueba E2E

A continuación, presentamos el archivo docker-compose.yml que orquesta nuestros cuatro microservicios y sus bases de datos. Nota la definición de una red común (ecommerce_net) y las dependencias de salud (healthcheck) para asegurar que un servicio solo se inicie cuando sus dependencias estén listas.

version: '3.8'
services:
  products-db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: productsdb
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    networks:
      - ecommerce_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin"]
      interval: 5s
      timeout: 5s
      retries: 5

  products-service:
    build: ./products-service
    ports:
      - "8081:8080"
    environment:
      DB_HOST: products-db
      DB_USER: admin
      DB_PASSWORD: secret
      DB_NAME: productsdb
      DB_PORT: 5432
    depends_on:
      products-db:
        condition: service_healthy
    networks:
      - ecommerce_net

  users-db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: usersdb
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    networks:
      - ecommerce_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin"]
      interval: 5s
      timeout: 5s
      retries: 5

  users-service:
    build: ./users-service
    ports:
      - "8082:8080"
    environment:
      DB_HOST: users-db
      DB_USER: admin
      DB_PASSWORD: secret
      DB_NAME: usersdb
      DB_PORT: 5432
      JWT_SECRET: my_super_secret_jwt_key_for_ecommerce
    depends_on:
      users-db:
        condition: service_healthy
    networks:
      - ecommerce_net

  orders-db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ordersdb
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    networks:
      - ecommerce_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin"]
      interval: 5s
      timeout: 5s
      retries: 5

  orders-service:
    build: ./orders-service
    ports:
      - "8083:8080"
    environment:
      DB_HOST: orders-db
      DB_USER: admin
      DB_PASSWORD: secret
      DB_NAME: ordersdb
      DB_PORT: 5432
      USER_SERVICE_URL: "http://users-service:8080"
      PRODUCT_SERVICE_URL: "http://products-service:8080"
    depends_on:
      - orders-db
      - users-service
      - products-service
    networks:
      - ecommerce_net

  cart-db:
    image: redis:7-alpine
    networks:
      - ecommerce_net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  cart-service:
    build: ./cart-service
    ports:
      - "8084:8080"
    environment:
      REDIS_HOST: cart-db
      REDIS_PORT: 6379
    depends_on:
      cart-db:
        condition: service_healthy
    networks:
      - ecommerce_net

networks:
  ecommerce_net:
    driver: bridge

Ahora, un extracto de un script de prueba de integración en Go (test_e2e.go). Este script realiza el flujo completo descrito anteriormente. Utiliza el paquete net/http para hacer las solicitudes y encoding/json para manejar los datos.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

const (
    baseURL          = "http://localhost"
    usersPort        = ":8082"
    productsPort     = ":8081"
    cartPort         = ":8084"
    ordersPort       = ":8083"
)

type UserCredentials struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

type AuthResponse struct {
    Token string `json:"token"`
}

type Product struct {
    ID    string  `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
    Stock int     `json:"stock"`
}

func main() {
    log.Println("=== Iniciando Prueba E2E del Sistema de E-commerce ===")

    // 1. Autenticación de usuario (o registro)
    token, err := authenticateUser("[email protected]", "password123")
    if err != nil {
        log.Fatalf("Fallo en autenticación: %v", err)
    }
    log.Printf("Token obtenido: %s...\n", token[:20])

    // 2. Obtener o crear un producto
    productID, err := getOrCreateProduct(token)
    if err != nil {
        log.Fatalf("Fallo con producto: %v", err)
    }
    log.Printf("Producto ID a usar: %s\n", productID)

    // 3. Añadir producto al carrito
    err = addToCart(token, productID, 2)
    if err != nil {
        log.Fatalf("Fallo al añadir al carrito: %v", err)
    }
    log.Println("Producto añadido al carrito correctamente.")

    // 4. Crear la orden
    orderID, err := createOrder(token)
    if err != nil {
        log.Fatalf("Fallo al crear la orden: %v", err)
    }
    log.Printf("¡Orden creada exitosamente! ID de Orden: %s\n", orderID)

    log.Println("=== Prueba E2E COMPLETADA CON ÉXITO ===")
}

func authenticateUser(email, password string) (string, error) {
    url := baseURL + usersPort + "/auth/login"
    creds := UserCredentials{Email: email, Password: password}
    body, _ := json.Marshal(creds)

    resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("código de estado inesperado: %d", resp.StatusCode)
    }

    var authResp AuthResponse
    if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
        return "", err
    }
    return authResp.Token, nil
}

// Las funciones getOrCreateProduct, addToCart y createOrder seguirían una estructura similar,
// construyendo las solicitudes HTTP con los headers apropiados (como Authorization: Bearer )
// y verificando las respuestas.
Tip Crítico: Nunca ejecutes pruebas E2E contra un entorno de producción. Utiliza un entorno de staging idéntico en configuración pero aislado en datos. Además, considera sembrar (seed) tus bases de datos con datos de prueba conocidos y limpiarlas después de cada ejecución de pruebas para garantizar la repetibilidad.

Errores Comunes y Cómo Evitarlos

1. Falta de Timeouts en las Llamadas entre Servicios: En una arquitectura de microservicios, una llamada HTTP de un servicio a otro puede colgarse indefinidamente si el servicio destino está saturado o tiene un problema. Si no configuras timeouts a nivel de cliente HTTP, tu goroutine quedará bloqueada, consumiendo recursos y potencialmente causando un efecto cascada de fallos. Cómo evitarlo: Siempre configura timeouts explícitos en el cliente HTTP de Go. Usa context.WithTimeout y pasa el contexto a la solicitud. Un timeout de 3-5 segundos suele ser un buen punto de partida para llamadas internas.

2. Asumir que los Servicios Dependientes Siempre Están Sanos: Tu script de prueba puede funcionar la primera vez, pero si el servicio de Usuarios se cae momentáneamente, el servicio de Órdenes empezará a fallar. Un error común es no implementar un patrón de resiliencia como circuit breaker o reintentos con backoff exponencial. Cómo evitarlo: Implementa un cliente robusto para las comunicaciones entre servicios. Usa librerías como github.com/sony/gobreaker para el circuit breaker y github.com/cenkalti/backoff/v4 para reintentos inteligentes. No retries en errores 4xx (del cliente), solo en 5xx (del servidor) y fallos de red.

3. Pruebas No Deterministas (Flaky Tests): Las pruebas E2E a veces fallan y a veces pasan sin cambios en el código. Esto suele deberse a dependencias de estado compartido (como un ID de producto que cambia), condiciones de carrera o tiempos de espera (sleeps) fijos insuficientes. Cómo evitarlo: Usa identificadores únicos generados en tiempo de prueba (como UUIDs). Para esperar por la disponibilidad de un servicio, usa polls con un límite de tiempo en lugar de time.Sleep fijo. Aísla los datos de prueba por ejecución.

4. No Verificar la Integridad de los Datos en Todo el Flujo: Puedes probar que el código de respuesta HTTP es 200, pero no verificar que los datos hayan fluido correctamente. ¿La orden creada tiene el precio correcto? ¿Se descontó el stock del producto? Cómo evitarlo: En tus pruebas, después de cada acción significativa, realiza solicitudes de "lectura" para verificar el estado. Después de crear la orden, haz un GET a la orden y verifica que el total sea (precio del producto * cantidad). Haz un GET al producto y verifica que el stock se redujo.

5. Ignorar los Logs y la Observabilidad: Durante la integración, los errores pueden ocurrir en cualquier servicio. Si no tienes una estrategia centralizada de logs (como enviarlos a un agregador: ELK Stack, Loki) y métricas, depurar un problema distribuido se convierte en una pesadilla de buscar en los logs de 4 contenedores diferentes. Cómo evitarlo: Estructura tus logs en un formato como JSON. Incluye un request_id o correlation_id único que se propague a través de todas las llamadas entre servicios. Usa middleware en gorilla/mux para inyectar este ID en el contexto de cada solicitud y registrarlo en cada log relacionado con esa solicitud.

Checklist de Dominio

Antes de considerar completa la integración de tu sistema, verifica que puedes marcar cada uno de estos puntos:

  • Puedo levantar todos los microservicios y sus dependencias con un solo comando: docker-compose up --build.
  • He escrito y ejecutado al menos un script de prueba E2E que simula un flujo de negocio completo (ej: login -> añadir al carrito -> checkout) y pasa consistentemente.
  • Cada servicio maneja adecuadamente la falta de disponibilidad de sus servicios dependientes (ej: Orders devuelve un error 503 si Users no responde, en lugar de colgarse).
  • Los datos persisten correctamente a través de reinicios de contenedores (los volúmenes de Docker están configurados para las bases de datos).
  • He probado el sistema bajo carga moderada (ej: con hey o wrk) y confirmado que no hay cuellos de botella evidentes en la comunicación entre servicios.
  • Todos los endpoints de salud (/health) de los servicios responden correctamente cuando son consultados por Docker Compose o un orquestador.
  • Los logs de todos los servicios son accesibles y muestran los correlation_id que permiten seguir una solicitud a través del sistema completo.
  • He simulado un rollback de una versión de un microservicio y confirmado que el sistema sigue funcionando (compatibilidad hacia atrás de los contratos de API).
De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión