Broker-Tests

Ein MQTT-Smoke-Test-Skript in einer Minute (Bash, Python, Node)

MQTT-Smoke-Test-Skripte zum Kopieren für Bash, Python und Node.js: verbinden, abonnieren, publizieren, empfangen und PASS oder FAIL mit CI-tauglichem Exit-Code.

Aktualisiert · 8 Min. Lesezeit

Ein MQTT-Smoke-Test-Skript in einer Minute (Bash, Python, Node)

Ein MQTT-Smoke-Test beantwortet in unter einer Minute eine einzige Frage: Akzeptiert dieser Broker meinen Client und stellt er genau jetzt eine Nachricht zu? Unten findest du drei Skripte zum Kopieren – Bash, Python und Node.js –, die sich verbinden, abonnieren, ein eindeutiges Token publizieren und auf dessen Rückkehr warten. Jedes gibt pro Schritt PASS/FAIL aus und endet mit 0 oder 1; dieselbe Datei läuft also auf deinem Laptop, in CI und als Cronjob.

Was der Smoke-Test beweist

SchrittBeweistTypischer Fehler
VerbindenDNS, Netzwerkpfad, Port, TLS, ZugangsdatenConnection refused, not authorised, TLS-Fehler
AbonnierenSubscribe-ACLs, gültiges TopicAll subscription requests were denied.
Publizieren (QoS 1)Publish-ACLs, Broker nimmt die Nachricht anTimeout beim Warten auf PUBACK
EmpfangenRouting funktioniert Ende-zu-EndeInnerhalb des Timeouts kommt nichts an

Jeder Lauf nutzt ein eindeutiges Topic unter testmqtt/smoke/ und ein zufälliges Token. So sehen parallele Läufe nie die Nachrichten der anderen, und alte Retained-Daten können kein falsches PASS erzeugen. Alle Skripte lesen dieselben Umgebungsvariablen:

Konfiguration
export MQTT_HOST=broker.connectmq.com   # your broker
export MQTT_PORT=8883                   # 1883 plain, 8883 TLS
export MQTT_USER=username               # optional
export MQTT_PASS=password               # optional
export MQTT_TIMEOUT=10                  # seconds per step

Bash (Mosquitto-Clients)

Benötigt nur mosquitto_pub und mosquitto_sub (apt install mosquitto-clients oder brew install mosquitto). Der Subscriber läuft im Hintergrund, wartet auf genau eine Nachricht (-C 1) und gibt nach dem Timeout auf (-W).

smoke.sh
#!/usr/bin/env bash
# MQTT smoke test: connect, subscribe, publish, receive. Exit 0 = pass.
set -u
HOST=${MQTT_HOST:-broker.connectmq.com}
PORT=${MQTT_PORT:-1883}
TIMEOUT=${MQTT_TIMEOUT:-10}
TOPIC="testmqtt/smoke/$(hostname -s)-$$-$RANDOM"
TOKEN="smoke-$(date +%s)-$RANDOM"
OUT=$(mktemp); ERR=$(mktemp)
trap 'rm -f "$OUT" "$ERR"' EXIT

ARGS=(-h "$HOST" -p "$PORT" -q 1)
[ -n "${MQTT_USER:-}" ] && ARGS+=(-u "$MQTT_USER" -P "${MQTT_PASS:-}")
[ "$PORT" = 8883 ] && ARGS+=(--capath /etc/ssl/certs)

fail() { echo "FAIL  $1"; exit 1; }
command -v mosquitto_sub >/dev/null || fail "mosquitto clients are not installed"

mosquitto_sub "${ARGS[@]}" -t "$TOPIC" -C 1 -W "$TIMEOUT" >"$OUT" 2>"$ERR" &
SUB=$!
sleep 1
kill -0 "$SUB" 2>/dev/null || fail "connect/subscribe: $(cat "$ERR")"
echo "PASS  connect + subscribe  $HOST:$PORT"

mosquitto_pub "${ARGS[@]}" -t "$TOPIC" -m "$TOKEN" 2>"$ERR" || fail "publish: $(cat "$ERR")"
echo "PASS  publish  $TOPIC"

wait "$SUB" || fail "receive: nothing within ${TIMEOUT}s $(cat "$ERR")"
grep -qx "$TOKEN" "$OUT" || fail "receive: unexpected payload '$(cat "$OUT")'"
echo "PASS  round trip  ${SECONDS}s total"

Ein erfolgreicher Lauf sieht so aus:

Ausgabe
PASS  connect + subscribe  broker.connectmq.com:8883
PASS  publish  testmqtt/smoke/build-01-4127-18832
PASS  round trip  1s total

Das einsekündige sleep gibt dem Abonnement Zeit, sich vor dem Publish zu registrieren. Es ist die einzige Abkürzung in dieser Version; die Python- und Node-Skripte warten stattdessen auf das echte SUBACK. Weitere Optionen von mosquitto_pub/mosquitto_sub findest du in den Beispielen zu mosquitto_pub und mosquitto_sub.

Python (paho-mqtt 2.x)

pip install "paho-mqtt>=2". Das Skript wartet auf jede Bestätigung – CONNACK, SUBACK, PUBACK – und prüft die Reason Codes. Ein abgelehnter Login oder ein verweigertes Abonnement schlägt also im richtigen Schritt fehl, mit der Meldung des Brokers selbst.

smoke.py
#!/usr/bin/env python3
"""MQTT smoke test: exit 0 if connect, subscribe, publish and receive all pass."""
import os, sys, threading, time, uuid
import paho.mqtt.client as mqtt

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
TIMEOUT = float(os.getenv("MQTT_TIMEOUT", "10"))
TOPIC = f"testmqtt/smoke/{uuid.uuid4().hex[:12]}"
TOKEN = uuid.uuid4().hex
connected, subscribed, received = threading.Event(), threading.Event(), threading.Event()
state = {}

def step(ok, name, detail=""):
    print(f"{'PASS' if ok else 'FAIL'}  {name}  {detail}".rstrip())
    if not ok:
        sys.exit(1)

def on_connect(client, userdata, flags, reason_code, properties):
    state["connack"] = reason_code
    connected.set()

def on_subscribe(client, userdata, mid, reason_code_list, properties):
    state["suback"] = reason_code_list[0]
    subscribed.set()

def on_message(client, userdata, msg):
    if msg.payload.decode(errors="replace") == TOKEN:
        received.set()

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"smoke-{TOKEN[:8]}")
client.on_connect, client.on_subscribe, client.on_message = on_connect, on_subscribe, on_message
if os.getenv("MQTT_USER"):
    client.username_pw_set(os.environ["MQTT_USER"], os.getenv("MQTT_PASS", ""))
if PORT == 8883:
    client.tls_set()

start = time.perf_counter()
try:
    client.connect(HOST, PORT, keepalive=30)
except Exception as exc:
    step(False, "connect", f"{HOST}:{PORT} {exc}")
client.loop_start()

ok = connected.wait(TIMEOUT) and not state["connack"].is_failure
step(ok, "connect", f"{HOST}:{PORT} {state.get('connack', 'timeout')}")

client.subscribe(TOPIC, qos=1)
ok = subscribed.wait(TIMEOUT) and not state["suback"].is_failure
step(ok, "subscribe", str(state.get("suback", "timeout")))

info = client.publish(TOPIC, TOKEN, qos=1)
info.wait_for_publish(TIMEOUT)
step(info.is_published(), "publish", TOPIC)

step(received.wait(TIMEOUT), "round trip", f"{(time.perf_counter() - start) * 1000:.0f} ms")
client.disconnect()
client.loop_stop()

Die Netzwerk-Schleife läuft in einem Daemon-Thread, daher beendet sys.exit(1) den Prozess sofort beim ersten fehlgeschlagenen Schritt. Du willst die Callback-Signaturen von 2.x auffrischen? Siehe das paho-mqtt-2-Tutorial.

Node.js (MQTT.js 5)

npm install mqtt. MQTT.js 5 bringt Promise-basierte Methoden mit (connectAsync, subscribeAsync, publishAsync, endAsync), die das Skript linear halten. Weil es eine URL erwartet, testet dieselbe Datei auch WebSockets: MQTT_URL=wss://broker.connectmq.com:8084/mqtt.

smoke.mjs
// MQTT smoke test: node smoke.mjs — exit code 0 = pass
import mqtt from 'mqtt';
import { randomUUID } from 'node:crypto';

const host = process.env.MQTT_HOST ?? 'broker.connectmq.com';
const port = process.env.MQTT_PORT ?? '1883';
const url = process.env.MQTT_URL ?? `${port === '8883' ? 'mqtts' : 'mqtt'}://${host}:${port}`;
const timeout = Number(process.env.MQTT_TIMEOUT ?? 10) * 1000;
const topic = `testmqtt/smoke/${randomUUID().slice(0, 12)}`;
const token = randomUUID();

const step = (ok, name, detail = '') => {
  console.log(`${ok ? 'PASS' : 'FAIL'}  ${name}  ${detail}`.trimEnd());
  if (!ok) process.exit(1);
};
const within = (promise, what) =>
  Promise.race([promise, new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`${what} timed out after ${timeout} ms`)), timeout))]);

const start = performance.now();
let client;
try {
  client = await within(mqtt.connectAsync(url, {
    clientId: `smoke-${token.slice(0, 8)}`,
    username: process.env.MQTT_USER,
    password: process.env.MQTT_PASS,
    reconnectPeriod: 0,
    connectTimeout: timeout,
  }, false), 'connect');
  step(true, 'connect', url);
} catch (err) {
  step(false, 'connect', `${url} ${err.message}`);
}

const received = new Promise((resolve) => {
  client.on('message', (t, payload) => {
    if (t === topic && payload.toString() === token) resolve();
  });
});

try {
  const [grant] = await within(client.subscribeAsync(topic, { qos: 1 }), 'subscribe');
  step(grant.qos < 128, 'subscribe', `granted QoS ${grant.qos}`);
  await within(client.publishAsync(topic, token, { qos: 1 }), 'publish');
  step(true, 'publish', topic);
  await within(received, 'receive');
  step(true, 'round trip', `${Math.round(performance.now() - start)} ms`);
} catch (err) {
  step(false, 'step', err.message);
}
await client.endAsync();

Ein gewährter QoS von 128 (0x80) bedeutet, dass der Broker das Abonnement abgelehnt hat – meist wegen einer ACL.

In CI und nach Zeitplan ausführen

Da die Skripte bei Fehlern mit einem Exit-Code ungleich null enden, passen sie direkt in jede Pipeline. Ein Schritt für GitHub Actions:

.github/workflows/smoke.yml (step)
- name: MQTT smoke test
  env:
    MQTT_HOST: broker.connectmq.com
    MQTT_PORT: "8883"
    MQTT_USER: ${{ secrets.MQTT_USER }}
    MQTT_PASS: ${{ secrets.MQTT_PASS }}
  run: |
    sudo apt-get update && sudo apt-get install -y mosquitto-clients
    bash smoke.sh

Führe ihn nach jedem Deployment aus, das den Broker, seine Zertifikate oder seine ACLs betrifft, und zusätzlich nach Zeitplan (zum Beispiel alle fünf Minuten) als synthetischen Check. MQTT-Tests in GitHub Actions zeigt einen kompletten Workflow mit Broker als Service-Container.

Einen Fehlschlag lesen

Fehlgeschlagener SchrittZuerst prüfen
connect, Connection refused / TimeoutHost, Port, Firewall; läuft der Listener?
connect, not authorised / bad user name or passwordZugangsdaten, oder anonymer Zugriff deaktiviert
connect, TLS-/ZertifikatsfehlerPort 8883 statt 1883, CA-Bundle, Hostname im Zertifikat
subscribe abgelehnt, gewährter QoS 128Subscribe-ACL des Test-Users
publish läuft in den TimeoutPublish-ACL (MQTT 5 liefert 0x87), überlasteter Broker
round trip läuft in den Timeout, vorherige Schritte bestandenAbweichendes Topic, Publish-ACL verwirft stillschweigend (MQTT 3.1.1), Routing über Bridge oder Cluster

Alle Verbindungsfehler und ihre Lösungen behandelt MQTT-Verbindungsfehler; Reason Codes entschlüsselst du mit dem MQTT-Reason-Code-Tool. Soll ein Mensch den Check von Hand wiederholen, führt der Online-MQTT-Client denselben Zyklus aus Verbinden, Abonnieren und Publizieren im Browser aus.

Smoke-Test-Nachrichten live verfolgen

Ein Smoke-Test ist der günstigste MQTT-Test, den du schreiben kannst, und der, den du am häufigsten ausführst. Halte ihn kurz, mach jeden Lauf eindeutig, scheitere laut mit dem Schritt, der kaputt ist, und häng ihn in CI und einen Zeitplan; die tiefergehenden Prüfungen der MQTT-Broker-Test-Checkliste bauen darauf auf.

Häufig gestellte Fragen

Was sollte ein MQTT-Smoke-Test prüfen?

Mindestens einen vollständigen Roundtrip: mit echten Zugangsdaten verbinden, abonnieren, ein eindeutiges Token publizieren und genau dieses Token innerhalb eines Timeouts zurückbekommen. Dieser eine Pfad prüft DNS, Netzwerk, TLS, Authentifizierung, ACLs und Nachrichten-Routing.

Was unterscheidet einen Smoke-Test von einem Health Check?

Ein Health Check bestätigt meist nur, dass der Broker läuft, oft durch einen Verbindungsaufbau oder das Auslesen einer Metrik. Ein Smoke-Test beweist, dass der Broker seine Arbeit macht, indem er eine echte Nachricht Ende-zu-Ende schickt. Viele Teams nutzen den Smoke-Test als Health Check.

Warum schlägt mein Smoke-Test nur manchmal fehl?

Die häufigste Ursache ist ein Publish, bevor das Abonnement bestätigt ist – die erste Nachricht geht dann verloren. Warte vor dem Publish auf das SUBACK, nutze QoS 1 und pro Lauf ein eigenes Topic, und gib dem Empfangsschritt einen realistischen Timeout.