Tests de brokers

Un script de smoke test MQTT en une minute (Bash, Python, Node)

Scripts de smoke test MQTT prêts à copier en Bash, Python et Node.js : connexion, abonnement, publication, réception, puis PASS ou FAIL avec code de sortie.

Mis à jour · 8 min de lecture

Un script de smoke test MQTT en une minute (Bash, Python, Node)

Un smoke test MQTT répond à une seule question en moins d'une minute : ce broker accepte-t-il mon client et livre-t-il un message en ce moment ? Voici trois scripts prêts à copier (Bash, Python et Node.js) qui se connectent, s'abonnent, publient un jeton unique et attendent son retour. Chacun affiche PASS/FAIL à chaque étape et se termine avec 0 ou 1 : le même fichier fonctionne sur votre poste, en CI et dans un cron.

Ce que prouve le smoke test

ÉtapeProuveÉchec typique
ConnexionDNS, chemin réseau, port, TLS, identifiantsConnection refused, not authorised, erreurs TLS
AbonnementACL d'abonnement, validité du topicAll subscription requests were denied.
Publication (QoS 1)ACL de publication, le broker accepte le messageTimeout en attente du PUBACK
RéceptionLe routage fonctionne de bout en boutRien n'arrive avant le timeout

Chaque exécution utilise un topic unique sous testmqtt/smoke/ et un jeton aléatoire : des exécutions parallèles ne voient jamais les messages des autres, et d'anciennes données retenues ne peuvent pas produire un faux succès. Tous les scripts lisent les mêmes variables d'environnement :

Configuration
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 (clients Mosquitto)

Il suffit de mosquitto_pub et mosquitto_sub (apt install mosquitto-clients ou brew install mosquitto). L'abonné tourne en arrière-plan, attend exactement un message (-C 1) et abandonne après le timeout (-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"

Une exécution réussie ressemble à ceci :

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

Le sleep d'une seconde laisse le temps à l'abonnement d'être enregistré avant la publication. C'est le seul raccourci de cette version ; les scripts Python et Node attendent le vrai SUBACK. Pour d'autres options de mosquitto_pub/mosquitto_sub, voir les exemples mosquitto_pub et mosquitto_sub.

Python (paho-mqtt 2.x)

pip install "paho-mqtt>=2". Le script attend chaque acquittement (CONNACK, SUBACK, PUBACK) et vérifie les reason codes : une connexion refusée ou un abonnement interdit échoue donc à la bonne étape, avec le message du broker lui-même.

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()

La boucle réseau tourne dans un thread daemon, donc sys.exit(1) termine le processus immédiatement à la première étape en échec. Besoin de revoir les signatures de callbacks de la version 2.x ? Voir le tutoriel paho-mqtt 2.

Node.js (MQTT.js 5)

npm install mqtt. MQTT.js 5 fournit des méthodes à base de promesses (connectAsync, subscribeAsync, publishAsync, endAsync) qui gardent le script linéaire. Comme il prend une URL, le même fichier teste aussi les 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();

Une QoS accordée de 128 (0x80) signifie que le broker a refusé l'abonnement, le plus souvent à cause d'une ACL.

L'exécuter en CI et de façon planifiée

Comme les scripts se terminent avec un code non nul en cas d'échec, ils s'intègrent directement dans n'importe quel pipeline. Une étape 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

Lancez-le après chaque déploiement qui touche le broker, ses certificats ou ses ACL, et de façon planifiée (par exemple toutes les cinq minutes) comme test synthétique. Les tests MQTT dans GitHub Actions présente un workflow complet avec un broker en service container.

Interpréter un échec

Étape en échecÀ vérifier en premier
connect, Connection refused / timeoutHôte, port, pare-feu ; le listener tourne-t-il ?
connect, not authorised / bad user name or passwordIdentifiants, ou accès anonyme désactivé
connect, erreur TLS / certificatPort 8883 au lieu de 1883, bundle de CA, nom d'hôte du certificat
subscribe refusé, QoS accordée 128ACL d'abonnement de l'utilisateur de test
publish expireACL de publication (MQTT 5 renvoie 0x87), broker surchargé
round trip expire alors que les étapes précédentes passentTopic différent, ACL de publication qui supprime silencieusement (MQTT 3.1.1), routage via bridge ou cluster

Toutes les erreurs de connexion et leurs correctifs sont traités dans les erreurs de connexion MQTT ; les reason codes se décodent avec l'outil des reason codes MQTT. Si vous voulez qu'une personne refasse le test à la main, le client MQTT en ligne effectue le même cycle connexion–abonnement–publication dans un navigateur.

Voir les messages du smoke test en direct

Un smoke test est le test MQTT le moins coûteux à écrire et celui que vous lancerez le plus souvent. Gardez-le court, rendez chaque exécution unique, échouez bruyamment en indiquant l'étape cassée, et branchez-le sur la CI et un planning ; les vérifications plus poussées de la checklist de test d'un broker MQTT peuvent s'appuyer dessus.

Questions fréquentes

Que doit vérifier un smoke test MQTT ?

Au minimum un aller-retour complet : se connecter avec de vrais identifiants, s'abonner, publier un jeton unique et recevoir exactement ce jeton avant un timeout. Ce seul chemin met à l'épreuve le DNS, le réseau, TLS, l'authentification, les ACL et le routage des messages.

Quelle différence entre un smoke test et un health check ?

Un health check confirme généralement que le broker est démarré, souvent en se connectant ou en lisant une métrique. Un smoke test prouve que le broker fait son travail en envoyant un vrai message de bout en bout. Beaucoup d'équipes utilisent le smoke test comme health check.

Pourquoi mon smoke test échoue-t-il seulement de temps en temps ?

La cause la plus fréquente est une publication avant l'acquittement de l'abonnement : le premier message est alors manqué. Attendez le SUBACK avant de publier, utilisez la QoS 1 et un topic unique par exécution, et donnez à l'étape de réception un timeout réaliste.