Um smoke test MQTT responde uma única pergunta em menos de um minuto: este broker aceita meu cliente e entrega uma mensagem agora? Abaixo estão três scripts prontos para copiar (Bash, Python e Node.js) que conectam, assinam, publicam um token único e esperam que ele volte. Cada um imprime PASS/FAIL por etapa e sai com 0 ou 1, então o mesmo arquivo funciona no seu notebook, no CI e em um cron job.
O que o smoke test prova
| Etapa | Prova | Falha típica |
|---|---|---|
| Conectar | DNS, caminho de rede, porta, TLS, credenciais | Connection refused, not authorised, erros de TLS |
| Assinar | ACLs de subscribe, validade do topic | All subscription requests were denied. |
| Publicar (QoS 1) | ACLs de publish, o broker aceita a mensagem | Timeout esperando o PUBACK |
| Receber | O roteamento funciona de ponta a ponta | Nada chega dentro do timeout |
Cada execução usa um topic único em testmqtt/smoke/ e um token aleatório, para que execuções paralelas nunca vejam as mensagens umas das outras e dados retidos antigos não gerem um falso positivo. Todos os scripts leem as mesmas variáveis de ambiente:
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 Mosquitto)
Precisa apenas de mosquitto_pub e mosquitto_sub (apt install mosquitto-clients ou brew install mosquitto). O subscriber roda em segundo plano, espera exatamente uma mensagem (-C 1) e desiste após o 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"Uma execução bem-sucedida fica assim:
PASS connect + subscribe broker.connectmq.com:8883
PASS publish testmqtt/smoke/build-01-4127-18832
PASS round trip 1s totalO sleep de um segundo dá tempo para a assinatura ser registrada antes do publish. É o único atalho desta versão; os scripts em Python e Node esperam o SUBACK de verdade. Para mais opções do mosquitto_pub/mosquitto_sub, veja os exemplos de mosquitto_pub e mosquitto_sub.
Python (paho-mqtt 2.x)
pip install "paho-mqtt>=2". O script espera cada confirmação (CONNACK, SUBACK, PUBACK) e verifica os reason codes, então um login recusado ou uma assinatura negada falha na etapa certa com a própria mensagem do 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()O loop de rede roda em uma thread daemon, então sys.exit(1) encerra o processo imediatamente na primeira etapa que falhar. Precisa relembrar as assinaturas de callback da versão 2.x? Veja o tutorial do paho-mqtt 2.
Node.js (MQTT.js 5)
npm install mqtt. O MQTT.js 5 traz métodos baseados em promises (connectAsync, subscribeAsync, publishAsync, endAsync), que deixam o script linear. Como ele recebe uma URL, o mesmo arquivo também testa 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();Um QoS concedido de 128 (0x80) significa que o broker recusou a assinatura, normalmente por causa de uma ACL.
Rode no CI e de forma agendada
Como os scripts saem com código diferente de zero em caso de falha, eles entram direto em qualquer pipeline. Uma etapa do 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.shRode-o após cada deploy que mexa no broker, nos certificados ou nas ACLs, e de forma agendada (por exemplo a cada cinco minutos) como verificação sintética. Testes MQTT no GitHub Actions mostra um workflow completo com um broker como service container.
Lendo uma falha
| Etapa que falhou | O que olhar primeiro |
|---|---|
connect, Connection refused / timeout | Host, porta, firewall; o listener está rodando? |
connect, not authorised / bad user name or password | Credenciais, ou acesso anônimo desativado |
| connect, erro de TLS / certificado | Porta 8883 vs 1883, bundle de CA, hostname no certificado |
| subscribe negado, QoS concedido 128 | ACL de subscribe do usuário de teste |
| publish dá timeout | ACL de publish (o MQTT 5 retorna 0x87), broker sobrecarregado |
| round trip dá timeout e as etapas anteriores passam | Topic diferente, ACL de publish descartando em silêncio (MQTT 3.1.1), roteamento de bridge ou cluster |
Todos os erros de conexão e suas correções estão em erros de conexão MQTT; os reason codes podem ser decodificados com a ferramenta de reason codes MQTT. Se você quiser que alguém repita a verificação manualmente, o cliente MQTT online faz o mesmo ciclo conectar–assinar–publicar no navegador.
Acompanhe as mensagens do smoke test ao vivo
Um smoke test é o teste MQTT mais barato que você pode escrever e o que você vai rodar com mais frequência. Mantenha-o curto, torne cada execução única, falhe de forma clara indicando a etapa que quebrou e coloque-o no CI e em um agendamento; as verificações mais profundas do checklist de testes de broker MQTT podem ser construídas sobre ele.
Perguntas frequentes
O que um smoke test MQTT deve verificar?
No mínimo, um ciclo completo: conectar com credenciais reais, assinar, publicar um token único e receber exatamente esse token de volta dentro de um timeout. Esse único caminho exercita DNS, rede, TLS, autenticação, ACLs e roteamento de mensagens.
Qual a diferença entre um smoke test e um health check?
Um health check normalmente confirma que o broker está no ar, muitas vezes conectando ou lendo uma métrica. Um smoke test prova que o broker faz o seu trabalho, enviando uma mensagem real de ponta a ponta. Muitas equipes usam o smoke test como health check.
Por que meu smoke test falha só às vezes?
A causa mais comum é publicar antes de a assinatura ser confirmada, perdendo a primeira mensagem. Espere o SUBACK antes de publicar, use QoS 1 e um topic único por execução, e dê ao passo de recebimento um timeout realista.



