Testes de brokers

Um script de smoke test MQTT de um minuto (Bash, Python, Node)

Scripts de smoke test MQTT prontos em Bash, Python e Node.js: conectam, assinam, publicam e recebem, depois imprimem PASS ou FAIL com código de saída para CI.

Atualizado · 8 min de leitura

Um script de smoke test MQTT de um minuto (Bash, Python, Node)

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

EtapaProvaFalha típica
ConectarDNS, caminho de rede, porta, TLS, credenciaisConnection refused, not authorised, erros de TLS
AssinarACLs de subscribe, validade do topicAll subscription requests were denied.
Publicar (QoS 1)ACLs de publish, o broker aceita a mensagemTimeout esperando o PUBACK
ReceberO roteamento funciona de ponta a pontaNada 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:

Configuração
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 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).

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"

Uma execução bem-sucedida fica assim:

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

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

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

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.

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

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:

.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

Rode-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 falhouO que olhar primeiro
connect, Connection refused / timeoutHost, porta, firewall; o listener está rodando?
connect, not authorised / bad user name or passwordCredenciais, ou acesso anônimo desativado
connect, erro de TLS / certificadoPorta 8883 vs 1883, bundle de CA, hostname no certificado
subscribe negado, QoS concedido 128ACL de subscribe do usuário de teste
publish dá timeoutACL de publish (o MQTT 5 retorna 0x87), broker sobrecarregado
round trip dá timeout e as etapas anteriores passamTopic 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.