Tests de brokers

Tester MQTT en CI avec GitHub Actions

Tests MQTT dans GitHub Actions : un conteneur Mosquitto qui accepte vraiment les connexions, puis des tests sur un broker hébergé via les secrets du dépôt.

Mis à jour · 9 min de lecture

Tester MQTT en CI avec GitHub Actions

Pour tester MQTT dans GitHub Actions, démarrez un broker dans le job, attendez qu'il accepte les connexions et lancez votre suite de tests contre lui — puis ajoutez un second job qui exécute les mêmes tests sur un broker hébergé, avec des identifiants stockés en secrets du dépôt. Le premier job détecte les bugs de logique à chaque commit ; le second détecte les problèmes de TLS, d'authentification et d'ACL avant vos appareils.

Ce guide fournit des workflows prêts à copier pour les deux cas et explique le réglage par défaut de Mosquitto qui fait échouer la plupart des premières tentatives.

Le piège du « local only mode »

Le premier réflexe est un service container :

Semble correct, mais ne fonctionne pas
services:
  mosquitto:
    image: eclipse-mosquitto:2
    ports:
      - 1883:1883

Le conteneur démarre, le port est mappé, et tous les tests échouent avec Connection refused ou ECONNRESET. Le log du broker explique pourquoi :

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.

Depuis la version 2.0, Mosquitto sans aucun listener dans sa configuration ne se lie qu'à l'interface loopback à l'intérieur de son propre conteneur. Le trafic qui arrive par le mappage de ports vient de l'extérieur : il est donc refusé. Le fichier de configuration par défaut de l'image a toutes ses options commentées, c'est donc exactement ce que vous obtenez.

Les service containers compliquent la correction : ils démarrent avant l'exécution du moindre step, votre dépôt (et son mosquitto.conf) n'est donc pas encore récupéré. L'approche la plus simple et la plus prévisible consiste à démarrer le broker vous-même dans un step. Le même problème en local est traité dans Mosquitto avec Docker et la configuration Mosquitto ne se charge pas.

Job 1 : Mosquitto dans le workflow

Committez une configuration de test minimale dans le dépôt :

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

Vous pouvez générer des variantes plus strictes — avec un fichier de mots de passe ou des ACL pour tester les refus — avec le générateur de configuration Mosquitto. Démarrez ensuite le broker après le checkout et attendez qu'il réponde réellement en MQTT, et pas seulement en 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

Pourquoi la boucle exécute mosquitto_pub au lieu de tester le port : le proxy de ports de Docker accepte les connexions TCP sur l'hôte dès que le conteneur existe, avant même que le broker n'écoute à l'intérieur. Une publication réussie prouve que le broker est opérationnel. L'image eclipse-mosquitto inclut les clients en ligne de commande, il n'y a donc rien d'autre à installer.

Pour un projet Node.js, remplacez les steps Python par actions/setup-node@v4, npm ci et npm test ; les steps du broker restent identiques.

Job 2 : tests sur un broker hébergé

Un conteneur en CI est anonyme, non chiffré et reconfiguré à chaque exécution — l'inverse de la production. Un second job exécute la même suite en TLS avec de vrais identifiants. Stockez-les dans Settings → Secrets and variables → Actions sous les noms MQTT_USER et MQTT_PASS :

.github/workflows/mqtt-tests.yml (suite)
  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/

Ce que fait chaque partie :

  • needs: local-broker n'exécute le job plus lent, qui passe par le réseau, que lorsque le job rapide a réussi.
  • La condition if: ignore le job pour les pull requests issues de forks, qui ne reçoivent pas de secrets. Sans elle, le job échoue avec une erreur d'authentification qui ressemble à un vrai bug.
  • MQTT_TOPIC_PREFIX inclut l'ID d'exécution et le numéro de tentative, pour que les relances et les workflows parallèles ne partagent jamais de topics. Vos tests doivent construire chaque topic à partir de lui — voir tests d'intégration MQTT pour le modèle de fixture.
  • GitHub masque automatiquement les valeurs des secrets dans les logs, mais évitez malgré tout d'afficher les options de connexion dans la sortie de debug.

Garder le job hébergé rapide et propre

Quelques habitudes gardent le job réseau fiable à mesure que la suite grandit :

  • Exécutez un sous-ensemble. Marquez les tests qui ont besoin du TLS, d'identifiants ou d'ACL (par exemple @pytest.mark.hosted ou un projet Jest séparé) et n'exécutez qu'eux dans le job hébergé. La logique pure est déjà couverte par le job local.
  • Nettoyez les retained messages. Chaque retained message créé par un test survit à l'exécution. Effacez-le au teardown avec une publication retained vide, ou évitez le flag retain en dehors des tests qui vérifient ce comportement.
  • Utilisez des sessions propres. Les sessions persistantes avec des client IDs aléatoires s'accumulent sur le broker jusqu'à leur expiration. Utilisez des sessions propres (ou une courte expiration de session en MQTT 5), sauf si le test porte sur les sessions.
  • Adaptez les timeouts au réseau. Un message qui arrive en 20 ms en local peut demander une seconde depuis un runner partagé via internet. Utilisez des timeouts de message de 5 à 10 secondes dans ce job ; les tests se terminent toujours dès l'arrivée du message.

Tester plusieurs brokers avec une matrice

Si votre bibliothèque ou votre gateway doit fonctionner avec plusieurs brokers, une matrice exécute la suite contre chacun sans dupliquer le YAML :

Extrait de la matrice
  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 et HiveMQ CE démarrent plus lentement que Mosquitto et leurs images n'incluent pas mosquitto_pub : la vérification de disponibilité s'exécute donc depuis le runner. Épinglez des tags d'image exacts dans vos vrais pipelines pour qu'une nouvelle version du broker ne casse pas votre build sans prévenir. Un petit wait_for_broker.py n'a qu'à répéter client.connect() jusqu'au succès ou à l'expiration du timeout.

Déboguer les exécutions de CI en échec

Erreur dans le log du jobCauseCorrectif
Connection refused / ECONNREFUSED juste après le démarrageBroker pas encore prêt, ou local only modeUtiliser la boucle de disponibilité MQTT ; déclarer un listener
ECONNRESET à chaque connexionPort mappé mais Mosquitto lié au loopbacklistener 1883 0.0.0.0 dans la configuration
Connection Refused: not authorised dans le job hébergéSecret manquant (PR de fork) ou mauvais utilisateurVérifier la condition if: et les noms des secrets
[SSL: CERTIFICATE_VERIFY_FAILED]Port TLS utilisé sans CA, ou mauvais nom d'hôteUtiliser le bundle de CA du système et le vrai nom d'hôte du broker
Les tests passent seuls mais échouent dans la matriceTopics partagés entre jobsPréfixe de topic avec l'ID d'exécution et la valeur de la matrice

Le step if: failure() qui affiche docker logs mérite d'être conservé en permanence : le log du broker montre les connexions rejetées, les refus d'ACL et les client IDs. Pour les reason codes des erreurs côté client, utilisez la recherche de reason codes MQTT.

Vérifier le pipeline

Faites prouver au pipeline qu'il sait échouer. Poussez une branche qui publie sur le mauvais topic et vérifiez que le job passe au rouge. Ensuite, pendant l'exécution du job hébergé, abonnez-vous à testmqtt/ci/# dans le client MQTT en ligne avec les mêmes identifiants : vous verrez apparaître le préfixe propre à l'exécution et circuler le trafic de test, ce qui confirme que les secrets, le TLS et les ACL fonctionnent comme prévu.

Observer le trafic des tests CI en direct

Un pipeline MQTT fiable a besoin de deux choses : un broker dans le job, configuré pour accepter les connexions et vérifié par une vraie publication, et une seconde étape qui met à l'épreuve le TLS, les identifiants et les ACL utilisés en production. Avec des préfixes de topic uniques et des secrets tenus à l'écart des builds de forks, les deux s'exécutent à chaque push sans se gêner. Si vous préférez gérer les conteneurs depuis le code de test lui-même, consultez tests MQTT avec Testcontainers.

Questions fréquentes

Pourquoi mon job GitHub Actions ne peut-il pas se connecter au conteneur Mosquitto ?

Mosquitto 2.x démarre en local only mode lorsqu'aucun listener n'est configuré : il n'accepte alors que les connexions provenant de son propre conteneur. Démarrez-le avec une configuration qui déclare listener 1883 et allow_anonymous true (ou de vrais identifiants), par exemple le fichier fourni /mosquitto-no-auth.conf.

Les secrets GitHub sont-ils disponibles pour les pull requests issues de forks ?

Non. Les workflows déclenchés par des pull requests provenant de dépôts forkés ne reçoivent pas les secrets du dépôt. Exécutez les tests sur le broker hébergé uniquement lors des pushes et des pull requests du même dépôt, et gardez le job avec broker local pour tout le reste.

En CI, faut-il utiliser un Mosquitto local ou un broker hébergé ?

Les deux. Un conteneur local donne un retour rapide et isolé à chaque commit. Un job sur un broker hébergé vérifie le TLS, l'authentification et les ACL tels qu'ils sont configurés en production, ce qu'un conteneur jetable ne peut pas faire.