Un smoke test MQTT responde una sola pregunta en menos de un minuto: ¿este broker acepta mi cliente y entrega un mensaje ahora mismo? A continuación tienes tres scripts listos para copiar (Bash, Python y Node.js) que se conectan, se suscriben, publican un token único y esperan a que vuelva. Cada uno imprime PASS/FAIL por paso y termina con 0 o 1, así que el mismo archivo funciona en tu portátil, en CI y en un cron job.
Qué demuestra el smoke test
| Paso | Demuestra | Fallo típico |
|---|---|---|
| Conectar | DNS, ruta de red, puerto, TLS, credenciales | Connection refused, not authorised, errores TLS |
| Suscribirse | ACL de suscripción, validez del topic | All subscription requests were denied. |
| Publicar (QoS 1) | ACL de publicación, el broker acepta el mensaje | Timeout esperando el PUBACK |
| Recibir | El enrutamiento funciona de extremo a extremo | No llega nada antes del timeout |
Cada ejecución usa un topic único bajo testmqtt/smoke/ y un token aleatorio, de modo que las ejecuciones en paralelo nunca ven los mensajes de las demás y los datos retenidos antiguos no pueden producir un falso positivo. Todos los scripts leen las mismas variables de entorno:
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 (clientes de Mosquitto)
Solo necesita mosquitto_pub y mosquitto_sub (apt install mosquitto-clients o brew install mosquitto). El suscriptor se ejecuta en segundo plano, espera exactamente un mensaje (-C 1) y se rinde al agotar el 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"Una ejecución correcta se ve así:
PASS connect + subscribe broker.connectmq.com:8883
PASS publish testmqtt/smoke/build-01-4127-18832
PASS round trip 1s totalEl sleep de un segundo da tiempo a que la suscripción quede registrada antes de publicar. Es el único atajo de esta versión; los scripts de Python y Node esperan al SUBACK real. Para más opciones de mosquitto_pub/mosquitto_sub, consulta los ejemplos de mosquitto_pub y mosquitto_sub.
Python (paho-mqtt 2.x)
pip install "paho-mqtt>=2". El script espera cada confirmación (CONNACK, SUBACK, PUBACK) y comprueba los reason codes, así que un login rechazado o una suscripción denegada falla en el paso correcto con el propio mensaje del broker.
#!/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()El bucle de red se ejecuta en un hilo daemon, así que sys.exit(1) termina el proceso inmediatamente en el primer paso que falla. ¿Necesitas repasar las firmas de los callbacks de la versión 2.x? Consulta el tutorial de paho-mqtt 2.
Node.js (MQTT.js 5)
npm install mqtt. MQTT.js 5 incluye métodos basados en promesas (connectAsync, subscribeAsync, publishAsync, endAsync) que mantienen el script lineal. Como recibe una URL, el mismo archivo también prueba 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();Un QoS concedido de 128 (0x80) significa que el broker rechazó la suscripción, normalmente por una ACL.
Ejecútalo en CI y de forma programada
Como los scripts terminan con un código distinto de cero al fallar, encajan directamente en cualquier pipeline. Un paso de 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.shEjecútalo tras cada despliegue que toque el broker, sus certificados o sus ACL, y de forma programada (por ejemplo cada cinco minutos) como comprobación sintética. Pruebas MQTT en GitHub Actions muestra un workflow completo con un broker como service container.
Cómo leer un fallo
| Paso que falla | Qué revisar primero |
|---|---|
connect, Connection refused / timeout | Host, puerto, firewall; ¿está activo el listener? |
connect, not authorised / bad user name or password | Credenciales, o acceso anónimo desactivado |
| connect, error TLS / de certificado | Puerto 8883 frente a 1883, bundle de CA, hostname del certificado |
| subscribe denegado, QoS concedido 128 | ACL de suscripción del usuario de prueba |
| publish agota el tiempo | ACL de publicación (MQTT 5 devuelve 0x87), broker sobrecargado |
| round trip agota el tiempo y los pasos anteriores pasan | Topic que no coincide, ACL de publicación que descarta en silencio (MQTT 3.1.1), enrutamiento de bridge o clúster |
Todos los errores de conexión y sus soluciones se tratan en errores de conexión MQTT; los reason codes se pueden decodificar con la herramienta de reason codes MQTT. Si quieres que una persona repita la prueba a mano, el cliente MQTT online realiza el mismo ciclo conectar–suscribir–publicar en un navegador.
Ver en directo los mensajes del smoke test
Un smoke test es la prueba MQTT más barata que puedes escribir y la que más a menudo ejecutarás. Mantenlo corto, haz que cada ejecución sea única, falla de forma clara indicando el paso que se rompió e intégralo en CI y en una ejecución programada; las comprobaciones más profundas del checklist de pruebas de un broker MQTT pueden construirse sobre él.
Preguntas frecuentes
¿Qué debería comprobar un smoke test MQTT?
Como mínimo, un ciclo completo: conectar con credenciales reales, suscribirse, publicar un token único y recibir exactamente ese token antes de un timeout. Ese único camino pone a prueba el DNS, la red, TLS, la autenticación, las ACL y el enrutamiento de mensajes.
¿En qué se diferencia un smoke test de un health check?
Un health check suele confirmar que el broker está levantado, a menudo conectando o leyendo una métrica. Un smoke test demuestra que el broker hace su trabajo enviando un mensaje real de extremo a extremo. Muchos equipos usan el smoke test como health check.
¿Por qué mi smoke test falla solo a veces?
La causa más habitual es publicar antes de que se confirme la suscripción, con lo que se pierde el primer mensaje. Espera al SUBACK antes de publicar, usa QoS 1 y un topic único por ejecución, y da al paso de recepción un timeout realista.



