Pruebas de brokers

Probar MQTT en CI con GitHub Actions

Ejecuta tests MQTT en GitHub Actions: arranca un Mosquitto que acepte conexiones de verdad y prueba contra un broker alojado con secrets del repositorio.

Actualizado · 9 min de lectura

Probar MQTT en CI con GitHub Actions

Para probar MQTT en GitHub Actions, arranca un broker dentro del job, espera a que acepte conexiones y ejecuta tu suite de tests contra él; después añade un segundo job que ejecute los mismos tests contra un broker alojado, con las credenciales guardadas como secrets del repositorio. El primer job detecta errores de lógica en cada commit; el segundo detecta problemas de TLS, autenticación y ACL antes de que los sufran tus dispositivos.

Esta guía te da workflows listos para copiar para ambos casos y explica el valor por defecto de Mosquitto que rompe la mayoría de primeros intentos.

La trampa del "local only mode"

Lo primero que se le ocurre a cualquiera es un service container:

Parece correcto, pero no funciona
services:
  mosquitto:
    image: eclipse-mosquitto:2
    ports:
      - 1883:1883

El contenedor arranca, el puerto se mapea y todos los tests fallan con Connection refused o ECONNRESET. El log del broker explica por qué:

docker logs
mosquitto version 2.0.x starting
Config loaded from /mosquitto/config/mosquitto.conf.
Starting in local only mode. Connections will only be possible from clients running on this machine.
Create a configuration file which defines a listener to allow remote access.

Desde la versión 2.0, Mosquitto sin ningún listener en su configuración solo se enlaza a la interfaz loopback dentro de su propio contenedor. El tráfico que llega a través del mapeo de puertos viene de fuera, así que se rechaza. El archivo de configuración por defecto de la imagen tiene todas las opciones comentadas, de modo que eso es exactamente lo que obtienes.

Los service containers complican la solución: arrancan antes de que se ejecute cualquier step, así que tu repositorio (y su mosquitto.conf) aún no se ha clonado. Lo más sencillo y predecible es arrancar el broker tú mismo en un step. El mismo problema en entornos locales se trata en Mosquitto en Docker y la configuración de Mosquitto no se carga.

Job 1: Mosquitto dentro del workflow

Sube al repositorio una configuración de pruebas mínima:

test/mosquitto.conf
listener 1883 0.0.0.0
allow_anonymous true
persistence false
log_dest stdout

Puedes generar variantes más estrictas —con un archivo de contraseñas o ACL para probar denegaciones— con el generador de configuración de Mosquitto. Después arranca el broker tras el checkout y espera hasta que responda de verdad por MQTT, no solo por TCP:

.github/workflows/mqtt-tests.yml
name: mqtt-tests

on:
  push:
  pull_request:

jobs:
  local-broker:
    runs-on: ubuntu-latest
    env:
      MQTT_HOST: localhost
      MQTT_PORT: "1883"
    steps:
      - uses: actions/checkout@v4

      - name: Start Mosquitto
        run: |
          docker run -d --name mosquitto -p 1883:1883 \
            -v "$PWD/test/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro" \
            eclipse-mosquitto:2
          for i in $(seq 1 30); do
            if docker exec mosquitto mosquitto_pub -h localhost -t testmqtt/ci/ready -m ok; then
              echo "broker ready"; exit 0
            fi
            sleep 1
          done
          echo "broker did not start"; docker logs mosquitto; exit 1

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - run: pip install -r requirements-dev.txt

      - run: pytest -v tests/

      - name: Broker logs
        if: failure()
        run: docker logs mosquitto

Por qué el bucle ejecuta mosquitto_pub en lugar de comprobar el puerto: el proxy de puertos de Docker acepta conexiones TCP en el host en cuanto existe el contenedor, incluso antes de que el broker esté escuchando dentro. Una publicación correcta demuestra que el broker está operativo. La imagen eclipse-mosquitto incluye los clientes de línea de comandos, así que no hay que instalar nada más.

En un proyecto Node.js, sustituye los steps de Python por actions/setup-node@v4, npm ci y npm test; los steps del broker no cambian.

Job 2: tests contra un broker alojado

Un contenedor en CI es anónimo, sin cifrar y recién configurado en cada ejecución: justo lo contrario de producción. Un segundo job ejecuta la misma suite sobre TLS con credenciales reales. Guárdalas en Settings → Secrets and variables → Actions como MQTT_USER y MQTT_PASS:

.github/workflows/mqtt-tests.yml (continuación)
  hosted-broker:
    runs-on: ubuntu-latest
    needs: local-broker
    # secrets are not available to pull requests from forks
    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
    concurrency:
      group: mqtt-hosted-${{ github.ref }}
      cancel-in-progress: true
    env:
      MQTT_HOST: broker.connectmq.com
      MQTT_PORT: "8883"
      MQTT_USER: ${{ secrets.MQTT_USER }}
      MQTT_PASS: ${{ secrets.MQTT_PASS }}
      MQTT_TOPIC_PREFIX: testmqtt/ci/${{ github.run_id }}-${{ github.run_attempt }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements-dev.txt
      - run: pytest -v tests/

Qué hace cada parte:

  • needs: local-broker ejecuta el job más lento y con red solo cuando el job rápido ha pasado.
  • La condición if: omite el job en los pull requests desde forks, que no reciben secrets. Sin ella, el job falla con un error de autenticación que parece un bug real.
  • MQTT_TOPIC_PREFIX incluye el ID de ejecución y el número de intento, así que las re-ejecuciones y los workflows en paralelo nunca comparten topics. Tus tests deben construir cada topic a partir de él; consulta pruebas de integración MQTT para ver el patrón de fixture.
  • GitHub enmascara automáticamente los valores de los secrets en los logs, pero aun así evita imprimir las opciones de conexión en la salida de depuración.

Mantener el job alojado rápido y ordenado

Unos cuantos hábitos mantienen fiable el job con red a medida que crece la suite:

  • Ejecuta un subconjunto. Marca los tests que necesitan TLS, credenciales o ACL (por ejemplo @pytest.mark.hosted o un proyecto de Jest aparte) y ejecuta solo esos en el job alojado. La lógica pura ya la cubre el job local.
  • Limpia los retained messages. Cada retained message que crea un test sobrevive a la ejecución. Bórralo en el teardown con una publicación retained vacía, o evita el flag retain fuera de los tests que comprueban ese comportamiento.
  • Usa sesiones limpias. Las sesiones persistentes con client IDs aleatorios se acumulan en el broker hasta que expiran. Usa sesiones limpias (o una expiración de sesión corta en MQTT 5) salvo que el test sea sobre sesiones.
  • Ajusta los timeouts a la red. Un mensaje que llega en 20 ms en local puede necesitar un segundo desde un runner compartido a través de internet. Usa timeouts de mensaje de 5 a 10 segundos en este job; los tests siguen terminando en cuanto llega el mensaje.

Probar contra varios brokers con una matriz

Si tu librería o tu gateway tiene que funcionar con más de un broker, una matriz ejecuta la suite contra cada uno sin duplicar YAML:

Extracto de la matriz
  compat:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        image: ["eclipse-mosquitto:2", "emqx/emqx:latest", "hivemq/hivemq-ce:latest"]
    steps:
      - uses: actions/checkout@v4
      - name: Start ${{ matrix.image }}
        run: docker run -d --name broker -p 1883:1883 ${{ matrix.image }} ${{ startsWith(matrix.image, 'eclipse-mosquitto') && 'mosquitto -c /mosquitto-no-auth.conf' || '' }}
      - name: Wait for MQTT
        run: |
          pip install paho-mqtt
          python scripts/wait_for_broker.py localhost 1883 --timeout 60
      - run: pip install -r requirements-dev.txt && pytest -v tests/

EMQX y HiveMQ CE tardan más en arrancar que Mosquitto y sus imágenes no incluyen mosquitto_pub, así que la comprobación de disponibilidad se ejecuta desde el runner. Fija tags de imagen exactos en pipelines reales para que una nueva versión del broker no rompa tu build sin avisar. Un pequeño wait_for_broker.py solo necesita repetir client.connect() hasta que funcione o se agote el timeout.

Depurar ejecuciones de CI fallidas

Error en el log del jobCausaSolución
Connection refused / ECONNREFUSED justo después de arrancarEl broker aún no está listo, o local only modeUsa el bucle de disponibilidad MQTT; declara un listener
ECONNRESET en cada conexiónPuerto mapeado pero Mosquitto enlazado a loopbacklistener 1883 0.0.0.0 en la configuración
Connection Refused: not authorised en el job alojadoFalta el secret (PR desde fork) o usuario incorrectoRevisa la condición if: y los nombres de los secrets
[SSL: CERTIFICATE_VERIFY_FAILED]Puerto TLS sin CA, o hostname incorrectoUsa el bundle de CA del sistema y el hostname real del broker
Los tests pasan solos y fallan en la matrizTopics compartidos entre jobsPrefijo de topic con el ID de ejecución y el valor de la matriz

Merece la pena dejar para siempre el step if: failure() que imprime docker logs: el log del broker muestra conexiones rechazadas, denegaciones de ACL y client IDs. Para los reason codes de los errores del cliente, usa la búsqueda de reason codes MQTT.

Verificar el pipeline

Haz que el pipeline demuestre que puede fallar. Sube una rama que publique en el topic equivocado y confirma que el job se pone en rojo. Después, mientras se ejecuta el job alojado, suscríbete a testmqtt/ci/# en el cliente MQTT online con las mismas credenciales: verás aparecer el prefijo específico de la ejecución y fluir el tráfico de los tests, lo que confirma que los secrets, el TLS y las ACL funcionan como esperas.

Ver en directo el tráfico de los tests de CI

Un pipeline MQTT fiable necesita dos cosas: un broker en el job configurado para aceptar conexiones y comprobado con una publicación real, y una segunda fase que ejercite el TLS, las credenciales y las ACL que usas en producción. Con prefijos de topic únicos y los secrets fuera de los builds de forks, ambos se ejecutan en cada push sin pisarse. Si prefieres gestionar los contenedores desde el propio código de test, consulta tests MQTT con Testcontainers.

Preguntas frecuentes

¿Por qué mi job de GitHub Actions no puede conectarse al contenedor de Mosquitto?

Mosquitto 2.x arranca en local only mode cuando no hay ningún listener configurado, así que solo acepta conexiones desde dentro de su propio contenedor. Arráncalo con una configuración que declare listener 1883 y allow_anonymous true (o credenciales reales), por ejemplo el archivo incluido /mosquitto-no-auth.conf.

¿Están disponibles los secrets de GitHub en los pull requests desde forks?

No. Los workflows lanzados por pull requests desde repositorios fork no reciben los secrets del repositorio. Ejecuta los tests contra el broker alojado solo en pushes y en pull requests del mismo repositorio, y deja el job con broker local para todo lo demás.

¿En CI conviene usar un Mosquitto local o un broker alojado?

Usa ambos. Un contenedor local da feedback rápido y aislado en cada commit. Un job contra un broker alojado verifica TLS, autenticación y ACL tal como están configurados en producción, algo que un contenedor desechable no puede hacer.