Pruebas de brokers

Un script de smoke test MQTT de un minuto (Bash, Python, Node)

Scripts de smoke test MQTT listos para copiar en Bash, Python y Node.js: conectan, se suscriben, publican y reciben, e imprimen PASS o FAIL con un exit code.

Actualizado · 8 min de lectura

Un script de smoke test MQTT de un minuto (Bash, Python, Node)

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

PasoDemuestraFallo típico
ConectarDNS, ruta de red, puerto, TLS, credencialesConnection refused, not authorised, errores TLS
SuscribirseACL de suscripción, validez del topicAll subscription requests were denied.
Publicar (QoS 1)ACL de publicación, el broker acepta el mensajeTimeout esperando el PUBACK
RecibirEl enrutamiento funciona de extremo a extremoNo 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:

Configuración
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 (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).

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"

Una ejecución correcta se ve así:

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

El 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.

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

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.

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

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:

.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

Ejecú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 fallaQué revisar primero
connect, Connection refused / timeoutHost, puerto, firewall; ¿está activo el listener?
connect, not authorised / bad user name or passwordCredenciales, o acceso anónimo desactivado
connect, error TLS / de certificadoPuerto 8883 frente a 1883, bundle de CA, hostname del certificado
subscribe denegado, QoS concedido 128ACL de suscripción del usuario de prueba
publish agota el tiempoACL de publicación (MQTT 5 devuelve 0x87), broker sobrecargado
round trip agota el tiempo y los pasos anteriores pasanTopic 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.