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
| Étape | Prouve | Échec typique |
|---|---|---|
| Connexion | DNS, chemin réseau, port, TLS, identifiants | Connection refused, not authorised, erreurs TLS |
| Abonnement | ACL d'abonnement, validité du topic | All subscription requests were denied. |
| Publication (QoS 1) | ACL de publication, le broker accepte le message | Timeout en attente du PUBACK |
| Réception | Le routage fonctionne de bout en bout | Rien 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 :
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 stepBash (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).
#!/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 :
PASS connect + subscribe broker.connectmq.com:8883
PASS publish testmqtt/smoke/build-01-4127-18832
PASS round trip 1s totalLe 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.
#!/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.
// 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 :
- 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.shLancez-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 / timeout | Hôte, port, pare-feu ; le listener tourne-t-il ? |
connect, not authorised / bad user name or password | Identifiants, ou accès anonyme désactivé |
| connect, erreur TLS / certificat | Port 8883 au lieu de 1883, bundle de CA, nom d'hôte du certificat |
| subscribe refusé, QoS accordée 128 | ACL d'abonnement de l'utilisateur de test |
| publish expire | ACL de publication (MQTT 5 renvoie 0x87), broker surchargé |
| round trip expire alors que les étapes précédentes passent | Topic 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.



