Broker testleri

Bir Dakikalık MQTT Smoke Test Betiği (Bash, Python, Node)

Bash, Python ve Node.js için hazır MQTT smoke test betikleri: bağlanır, abone olur, yayınlar, alır ve CI uyumlu çıkış koduyla PASS veya FAIL yazdırır.

Güncellendi · 8 dk okuma

Bir Dakikalık MQTT Smoke Test Betiği (Bash, Python, Node)

Bir MQTT smoke testi tek bir soruyu bir dakikadan kısa sürede yanıtlar: Bu broker şu anda istemcimi kabul edip bir mesajı teslim ediyor mu? Aşağıda bağlanan, abone olan, benzersiz bir token yayınlayan ve onun geri gelmesini bekleyen üç kopyala-yapıştır betik var: Bash, Python ve Node.js. Her biri adım başına PASS/FAIL yazdırır ve 0 ya da 1 ile çıkar; böylece aynı dosya dizüstü bilgisayarınızda, CI'da ve bir cron job'ında çalışır.

Smoke test neyi kanıtlar?

AdımKanıtladığıTipik hata
BağlanmaDNS, ağ yolu, port, TLS, kimlik bilgileriConnection refused, not authorised, TLS hataları
Abone olmaSubscribe ACL'leri, topic geçerliliğiAll subscription requests were denied.
Yayınlama (QoS 1)Publish ACL'leri, broker'ın mesajı kabul etmesiPUBACK beklerken zaman aşımı
AlmaYönlendirmenin uçtan uca çalışmasıZaman aşımı içinde hiçbir şey gelmez

Her çalıştırma testmqtt/smoke/ altında benzersiz bir topic ve rastgele bir token kullanır; böylece paralel çalıştırmalar birbirlerinin mesajlarını görmez ve eski retained veriler yanlış bir başarıya yol açamaz. Tüm betikler aynı ortam değişkenlerini okur:

Yapılandırma
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 (Mosquitto istemcileri)

Yalnızca mosquitto_pub ve mosquitto_sub gerekir (apt install mosquitto-clients veya brew install mosquitto). Subscriber arka planda çalışır, tam olarak bir mesaj bekler (-C 1) ve zaman aşımından sonra vazgeçer (-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"

Başarılı bir çalıştırma şöyle görünür:

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

Bir saniyelik sleep, publish'ten önce aboneliğin kaydedilmesine zaman tanır. Bu sürümdeki tek kestirme budur; Python ve Node betikleri bunun yerine gerçek SUBACK'i bekler. Daha fazla mosquitto_pub/mosquitto_sub seçeneği için mosquitto_pub ve mosquitto_sub örnekleri makalesine bakın.

Python (paho-mqtt 2.x)

pip install "paho-mqtt>=2". Betik her onayı (CONNACK, SUBACK, PUBACK) bekler ve reason code'ları kontrol eder; böylece reddedilen bir giriş veya engellenen bir abonelik, broker'ın kendi mesajıyla doğru adımda başarısız olur.

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

Ağ döngüsü bir daemon thread'inde çalıştığı için sys.exit(1), ilk başarısız adımda süreci hemen sonlandırır. 2.x callback imzalarını hatırlamanız mı gerekiyor? paho-mqtt 2 eğitimine göz atın.

Node.js (MQTT.js 5)

npm install mqtt. MQTT.js 5, betiği doğrusal tutan promise tabanlı metotlarla gelir (connectAsync, subscribeAsync, publishAsync, endAsync). URL aldığı için aynı dosya WebSockets'i de test eder: 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();

128 (0x80) değerinde verilen bir QoS, broker'ın aboneliği reddettiği anlamına gelir; genellikle bir ACL yüzünden.

CI'da ve zamanlanmış olarak çalıştırın

Betikler hata durumunda sıfırdan farklı bir kodla çıktığı için doğrudan her pipeline'a eklenebilir. Bir GitHub Actions adımı:

.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

Broker'a, sertifikalarına veya ACL'lerine dokunan her deploy'dan sonra ve sentetik bir kontrol olarak zamanlanmış şekilde (örneğin beş dakikada bir) çalıştırın. GitHub Actions'ta MQTT testleri, service container olarak broker içeren eksiksiz bir workflow gösterir.

Bir hatayı okumak

Başarısız adımİlk bakılacak yer
connect, Connection refused / zaman aşımıHost, port, firewall; listener çalışıyor mu?
connect, not authorised / bad user name or passwordKimlik bilgileri ya da anonim erişimin kapalı olması
connect, TLS / sertifika hatası1883 yerine 8883 portu, CA paketi, sertifikadaki hostname
subscribe reddedildi, verilen QoS 128Test kullanıcısının subscribe ACL'i
publish zaman aşımına uğruyorPublish ACL'i (MQTT 5 0x87 döndürür), aşırı yüklü broker
round trip zaman aşımına uğruyor, önceki adımlar geçiyorUyuşmayan topic, mesajı sessizce atan publish ACL'i (MQTT 3.1.1), bridge veya cluster yönlendirmesi

Tüm bağlantı hataları ve çözümleri MQTT bağlantı hataları makalesinde ele alınır; reason code'ları MQTT reason code aracı ile çözebilirsiniz. Kontrolü birinin elle tekrarlamasını istiyorsanız online MQTT istemcisi aynı bağlan–abone ol–yayınla döngüsünü tarayıcıda gerçekleştirir.

Smoke test mesajlarını canlı izleyin

Smoke test, yazabileceğiniz en ucuz MQTT testidir ve en sık çalıştıracağınız test de odur. Kısa tutun, her çalıştırmayı benzersiz yapın, bozulan adımı belirterek net biçimde başarısız olun ve CI'a ve bir zamanlamaya bağlayın; MQTT broker test kontrol listesindeki daha derin kontroller bunun üzerine kurulabilir.

Sıkça sorulan sorular

Bir MQTT smoke testi neyi kontrol etmeli?

En azından tam bir gidiş-dönüş: gerçek kimlik bilgileriyle bağlanmak, abone olmak, benzersiz bir token yayınlamak ve tam olarak o token'ı bir zaman aşımı içinde geri almak. Bu tek yol DNS'i, ağı, TLS'i, kimlik doğrulamayı, ACL'leri ve mesaj yönlendirmesini sınar.

Smoke test ile health check arasındaki fark nedir?

Health check genellikle broker'ın ayakta olduğunu, çoğu zaman bağlanarak ya da bir metrik okuyarak doğrular. Smoke test ise uçtan uca gerçek bir mesaj göndererek broker'ın işini yaptığını kanıtlar. Birçok ekip smoke testi health check olarak kullanır.

Smoke testim neden yalnızca bazen başarısız oluyor?

En yaygın neden, abonelik onaylanmadan publish yapılması ve bu yüzden ilk mesajın kaçırılmasıdır. Publish'ten önce SUBACK'i bekleyin, QoS 1 ve her çalıştırma için benzersiz bir topic kullanın, alma adımına da gerçekçi bir zaman aşımı verin.