Broker-Tests

MQTT in CI mit GitHub Actions testen

MQTT-Tests in GitHub Actions: Starte einen Mosquitto-Container, der wirklich Verbindungen annimmt, und teste gegen einen gehosteten Broker mit Secrets.

Aktualisiert · 9 Min. Lesezeit

MQTT in CI mit GitHub Actions testen

Um MQTT in GitHub Actions zu testen, startest du einen Broker im Job, wartest, bis er Verbindungen annimmt, und lässt deine Testsuite dagegen laufen – dann ergänzt du einen zweiten Job, der dieselben Tests gegen einen gehosteten Broker ausführt, mit Zugangsdaten als Repository-Secrets. Der erste Job findet Logikfehler bei jedem Commit; der zweite findet TLS-, Authentifizierungs- und ACL-Probleme, bevor deine Geräte sie bemerken.

Diese Anleitung gibt dir kopierfertige Workflows für beides und erklärt die eine Mosquitto-Voreinstellung, an der die meisten ersten Versuche scheitern.

Die „Local Only Mode"-Falle

Der erste Impuls ist ein Service-Container:

Sieht richtig aus, funktioniert aber nicht
services:
  mosquitto:
    image: eclipse-mosquitto:2
    ports:
      - 1883:1883

Der Container startet, der Port ist gemappt, und jeder Test schlägt mit Connection refused oder ECONNRESET fehl. Das Broker-Log erklärt, warum:

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.

Seit Version 2.0 bindet sich Mosquitto ohne listener in der Konfiguration nur an das Loopback-Interface innerhalb seines eigenen Containers. Traffic, der über das Port-Mapping ankommt, kommt von außen und wird deshalb abgelehnt. Die Standard-Konfigurationsdatei des Images hat alle Optionen auskommentiert – genau das bekommst du also.

Service-Container machen die Lösung umständlich: Sie starten, bevor irgendein Step läuft, dein Repository (und seine mosquitto.conf) ist also noch nicht ausgecheckt. Am einfachsten und vorhersehbarsten ist es, den Broker selbst in einem Step zu starten. Dasselbe Problem für lokale Setups behandeln Mosquitto in Docker und Mosquitto-Konfiguration wird nicht geladen.

Job 1: Mosquitto im Workflow

Committe eine minimale Testkonfiguration ins Repository:

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

Strengere Varianten – mit Passwortdatei oder ACLs, um Ablehnungen zu testen – erzeugst du mit dem Mosquitto-Config-Generator. Starte den Broker dann nach dem Checkout und warte, bis er wirklich per MQTT antwortet, nicht nur per 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

Warum die Schleife mosquitto_pub ausführt, statt den Port zu prüfen: Dockers Port-Proxy nimmt TCP-Verbindungen auf dem Host an, sobald der Container existiert – noch bevor der Broker darin lauscht. Ein erfolgreicher Publish beweist, dass der Broker läuft. Das eclipse-mosquitto-Image bringt die Kommandozeilen-Clients mit, es muss also nichts zusätzlich installiert werden.

Für ein Node.js-Projekt ersetzt du die Python-Steps durch actions/setup-node@v4, npm ci und npm test; die Broker-Steps bleiben gleich.

Job 2: Tests gegen einen gehosteten Broker

Ein Container in CI ist anonym, unverschlüsselt und bei jedem Lauf frisch konfiguriert – das Gegenteil von Produktion. Ein zweiter Job führt dieselbe Suite über TLS mit echten Zugangsdaten aus. Speichere sie unter Settings → Secrets and variables → Actions als MQTT_USER und MQTT_PASS:

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

Was die einzelnen Teile tun:

  • needs: local-broker führt den langsameren Job mit Netzwerkzugriff nur aus, wenn der schnelle Job erfolgreich war.
  • Die if:-Bedingung überspringt den Job bei Pull Requests aus Forks, die keine Secrets erhalten. Ohne sie scheitert der Job mit einem Authentifizierungsfehler, der wie ein echter Bug aussieht.
  • MQTT_TOPIC_PREFIX enthält Run-ID und Attempt, sodass sich Wiederholungen und parallele Workflows nie Topics teilen. Deine Tests müssen jedes Topic daraus aufbauen – siehe MQTT-Integrationstests für das Fixture-Muster.
  • GitHub maskiert Secret-Werte in Logs automatisch, trotzdem solltest du Verbindungsoptionen nicht in Debug-Ausgaben schreiben.

Den gehosteten Job schnell und sauber halten

Ein paar Gewohnheiten halten den Netzwerk-Job zuverlässig, während die Suite wächst:

  • Nur eine Teilmenge ausführen. Markiere Tests, die TLS, Zugangsdaten oder ACLs brauchen (zum Beispiel @pytest.mark.hosted oder ein separates Jest-Projekt), und führe nur diese im gehosteten Job aus. Reine Logik deckt der lokale Job bereits ab.
  • Retained Messages aufräumen. Jede Retained Message, die ein Test erzeugt, überlebt den Lauf. Lösche sie im Teardown mit einem leeren Retained-Publish oder verzichte außerhalb von Retained-Tests auf das Retain-Flag.
  • Clean Sessions verwenden. Persistente Sessions mit zufälligen Client-IDs sammeln sich auf dem Broker, bis sie ablaufen. Nutze Clean Sessions (oder eine kurze MQTT-5-Session-Expiry), außer ein Test dreht sich um Sessions.
  • Timeouts an das Netzwerk anpassen. Eine Nachricht, die lokal in 20 ms ankommt, kann von einem geteilten Runner über das Internet eine Sekunde brauchen. Nutze in diesem Job Nachrichten-Timeouts von 5–10 Sekunden; die Tests enden trotzdem, sobald die Nachricht da ist.

Mit einer Matrix gegen mehrere Broker testen

Wenn deine Bibliothek oder dein Gateway mit mehr als einem Broker funktionieren muss, führt eine Matrix die Suite gegen jeden aus, ohne YAML zu duplizieren:

Matrix-Auszug
  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 und HiveMQ CE brauchen länger zum Starten als Mosquitto, und ihre Images enthalten kein mosquitto_pub, deshalb läuft der Bereitschafts-Check vom Runner aus. Pinne in echten Pipelines exakte Image-Tags, damit ein neues Broker-Release deinen Build nicht unangekündigt bricht. Ein kleines wait_for_broker.py muss nur client.connect() wiederholen, bis es klappt oder der Timeout abläuft.

Fehlgeschlagene CI-Läufe debuggen

Fehler im Job-LogUrsacheLösung
Connection refused / ECONNREFUSED direkt nach dem StartBroker noch nicht bereit oder Local Only ModeMQTT-Bereitschaftsschleife nutzen; Listener deklarieren
ECONNRESET bei jedem ConnectPort gemappt, aber Mosquitto an Loopback gebundenlistener 1883 0.0.0.0 in der Konfiguration
Connection Refused: not authorised im gehosteten JobSecret fehlt (Fork-PR) oder falscher Userif:-Bedingung und Secret-Namen prüfen
[SSL: CERTIFICATE_VERIFY_FAILED]TLS-Port ohne CA oder falscher HostnameSystem-CA-Bundle und den echten Hostnamen des Brokers verwenden
Tests laufen einzeln, scheitern in der MatrixGemeinsame Topics zwischen JobsTopic-Präfix mit Run-ID und Matrix-Wert

Der if: failure()-Step, der docker logs ausgibt, lohnt sich dauerhaft: Das Broker-Log zeigt abgelehnte Verbindungen, ACL-Ablehnungen und Client-IDs. Für Reason Codes in Client-Fehlern nutze die MQTT-Reason-Code-Suche.

Die Pipeline überprüfen

Lass die Pipeline beweisen, dass sie fehlschlagen kann. Pushe einen Branch, der auf das falsche Topic publisht, und bestätige, dass der Job rot wird. Abonniere dann, während der gehostete Job läuft, testmqtt/ci/# im Online-MQTT-Client mit denselben Zugangsdaten: Du siehst das laufspezifische Präfix auftauchen und den Test-Traffic fließen – damit ist bestätigt, dass Secrets, TLS und ACLs wie gewünscht funktionieren.

CI-Test-Traffic live ansehen

Eine zuverlässige MQTT-Pipeline braucht zwei Dinge: einen Broker im Job, der so konfiguriert ist, dass er Verbindungen annimmt, und der mit einem echten Publish geprüft wird, sowie eine zweite Stufe, die TLS, Zugangsdaten und ACLs wie in Produktion durchspielt. Mit eindeutigen Topic-Präfixen und ohne Secrets in Fork-Builds laufen beide bei jedem Push, ohne sich in die Quere zu kommen. Wenn du Container lieber direkt aus dem Testcode verwaltest, sieh dir MQTT-Tests mit Testcontainers an.

Häufig gestellte Fragen

Warum kann sich mein GitHub-Actions-Job nicht mit dem Mosquitto-Container verbinden?

Mosquitto 2.x startet im Local Only Mode, wenn kein Listener konfiguriert ist, und akzeptiert dann nur Verbindungen aus dem eigenen Container. Starte ihn mit einer Konfiguration, die listener 1883 und allow_anonymous true (oder echte Zugangsdaten) festlegt, zum Beispiel mit der mitgelieferten /mosquitto-no-auth.conf.

Sind GitHub-Secrets in Pull Requests aus Forks verfügbar?

Nein. Workflows, die durch Pull Requests aus geforkten Repositories ausgelöst werden, erhalten keine Repository-Secrets. Führe Tests gegen den gehosteten Broker nur bei Pushes und Pull Requests aus demselben Repository aus und nutze für alles andere den Job mit lokalem Broker.

Sollte CI einen lokalen Mosquitto oder einen gehosteten Broker verwenden?

Beides. Ein lokaler Container liefert bei jedem Commit schnelles, isoliertes Feedback. Ein Job gegen einen gehosteten Broker prüft TLS, Authentifizierung und ACLs so, wie sie in Produktion konfiguriert sind – das kann ein Wegwerf-Container nicht.