Tek başına geçen ama birlikte başarısız olan MQTT testleri neredeyse her zaman topic paylaşıyordur. İki CI job'ı test/temp topic'ine publish eder, dünden kalan bir retained mesaj bugünkü assertion'a yanıt verir ya da # üzerindeki bir subscriber başkasının trafiğini yutar. Çözüm bir adlandırma kuralıdır: her çalıştırma testmqtt/<suite>/<run-id>/ gibi benzersiz bir önek alır, testler yalnızca bunun altına subscribe olur ve teardown retained kalan her şeyi siler.
Bu rehber ölçeklenen bir adlandırma şemasını, run ID'lerin nasıl oluşturulacağını, Python ve JavaScript için yardımcıları ve çöken çalıştırmalardan kalanlar dahil retained test verilerinin nasıl temizleneceğini gösteriyor.
MQTT testleri neden çakışır
Broker paylaşılan bir durumdur. Bellek içi bir mock'tan farklı olarak hatırlar ve dağıtır; bu yüzden testler birbirine birkaç öngörülebilir yoldan sızar:
- Paralel çalıştırmalar: iki pipeline, iki geliştirici ya da iki pytest worker'ı aynı topic'e publish eder ve birbirinin mesajlarını alır.
- Retained artıklar: bir retained mesaj, onu oluşturan testten sonra da yaşar ve bir sonraki subscriber'a anında iletilir; böylece bozuk bir özellik çalışıyormuş gibi görünür.
- Fazla geniş wildcard'lar:
sensors/#topic'ine subscribe olan bir test, broker'daki diğer tüm testlerin sensör mesajlarını alır. - Tekrar kullanılan client ID'ler: aynı client ID'yi kullanan iki çalıştırma birbirini broker'dan atar, kalıcı oturumlar da önceki bir çalıştırmadan kuyrukta kalan mesajları iletir.
- Paylaşılan public broker'lar: herkese açık bir sandbox'ta yabancılar sizinle aynı "akla ilk gelen" topic adlarını kullanıyor olabilir.
Ölçeklenen bir adlandırma şeması
Test topic'lerini en genelden en özele doğru sabit seviyelerden oluşturun:
| Seviye | Örnek | Amaç |
|---|---|---|
| Kök | testmqtt | Tüm test trafiğini gerçek trafikten ayırır; ACL ile kısıtlaması kolaydır |
| Suite | orders | Topic'in hangi test paketine veya servise ait olduğu |
| Run ID | 20261017T101500-gh812-a3f9 | Her test çalıştırması için tek bir değer, makineler arasında benzersiz |
| Senaryo | test-retained-status | İsteğe bağlı: her test senaryosu için bir alt ağaç |
| Mantıksal topic | device/42/status | Kodunuzun üretimde kullanacağı topic |
Sonuç testmqtt/orders/20261017T101500-gh812-a3f9/test-retained-status/device/42/status olur. Uzun görünür, ama sondaki mantıksal kısım değişmez: test edilen kod yalnızca bir önek alır; bu, ortamları ayırmak için kullanılan mekanizmanın aynısıdır (bkz. geliştirme, staging ve üretim ortamlarını kurmak).
Birkaç kural bu adları güvenli tutar:
- Yalnızca küçük harf ASCII, rakamlar,
-ve_; boşluk yok, başta/yok, bir adın içinde asla+veya#yok. $ile başlamayın:$ile başlayan topic'ler broker'a ayrılmıştır ($SYS) ve#subscription'larının dışında kalır.- Her seviyeyi kısa tutun. Topic'ler her PUBLISH paketinde taşınır; genel kurallar için topic tasarımı en iyi uygulamaları yazısına bakın.
Run ID seçimi
| Kaynak | Örnek | Artıları | Eksileri |
|---|---|---|---|
| Rastgele UUID | 5f0c9e2a… | Her yerde benzersiz | Sıralanamaz, loglarda okunması zor |
| CI çalıştırma numarası | gh812-1 | Topic'leri bir pipeline çalıştırmasına bağlar | Dizüstü bilgisayarlarda yoktur; repolar arasında benzersiz değildir |
| Zaman damgası + rastgele | 20261017T101500-a3f9 | Sıralanabilir, okunur, her yerde çalışır | Biraz daha uzun |
En iyi seçim bunları birleştirir: önce bir UTC zaman damgası (kalıntılar yaşa göre temizlenebilsin diye), varsa CI çalıştırma numarası ve deneme sayısı, ardından aynı saniyede başlayan çalıştırmaları ayırmak için birkaç rastgele hex karakter.
Python: pytest için önek fixture'ı
import os, re, secrets, time
import pytest
import paho.mqtt.client as mqtt
HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
SUITE = "orders"
def make_run_id() -> str:
rid = time.strftime("%Y%m%dT%H%M%S", time.gmtime())
if os.getenv("GITHUB_RUN_ID"):
rid += f"-gh{os.environ['GITHUB_RUN_ID']}-{os.getenv('GITHUB_RUN_ATTEMPT', '1')}"
return f"{rid}-{secrets.token_hex(2)}"
RUN_ID = make_run_id()
def clear_retained(client, prefix, settle=1.5):
found = set()
def collect(c, userdata, msg):
if msg.retain and msg.payload:
found.add(msg.topic)
client.message_callback_add(f"{prefix}/#", collect)
client.subscribe(f"{prefix}/#", qos=1)
time.sleep(settle) # retained mesajlar SUBACK'ten hemen sonra gelir
client.unsubscribe(f"{prefix}/#")
client.message_callback_remove(f"{prefix}/#")
for topic in found:
client.publish(topic, b"", qos=1, retain=True).wait_for_publish(5)
return found
@pytest.fixture
def prefix(request):
case = re.sub(r"[^a-z0-9_-]+", "-", request.node.name.lower()).strip("-")
return f"testmqtt/{SUITE}/{RUN_ID}/{case}"
@pytest.fixture
def mqttc(prefix):
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id=f"t-{RUN_ID}-{secrets.token_hex(3)}")
if os.getenv("MQTT_USER"):
c.username_pw_set(os.environ["MQTT_USER"], os.environ["MQTT_PASS"])
c.connect(HOST, 1883)
c.loop_start()
yield c
clear_retained(c, prefix)
c.loop_stop()
c.disconnect()Böylece bir test her topic'i prefix üzerinden oluşturur ve hiçbirini koda gömmez. Test adı önekin parçası olduğu için parametreli testler ve paralel worker'lar (pytest-xdist) otomatik olarak ayrı kalır. Daha fazla fixture deseni için MQTT için pytest fixture'ları yazısına bakın.
JavaScript: Jest veya Vitest için aynı fikir
import { randomBytes } from "node:crypto";
const ts = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15); // 20261017T101500
const ci = process.env.GITHUB_RUN_ID ? `-gh${process.env.GITHUB_RUN_ID}` : "";
export const RUN_ID = `${ts}${ci}-${randomBytes(2).toString("hex")}`;
export const testPrefix = (suite, name) =>
`testmqtt/${suite}/${RUN_ID}/${name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;MQTT.js 5 ile retained veri publish etmesi gereken bir test, teardown hiç çalışmasa bile broker'ın veriyi atması için bir süre sonu da ekleyebilir:
client.publish(`${prefix}/device/42/status`, "online", {
qos: 1,
retain: true,
properties: { messageExpiryInterval: 3600 }, // yalnızca MQTT 5
});Mesaj süre sonu, bir MQTT 5 bağlantısı (protocolVersion: 5) ve bunu destekleyen bir broker gerektirir; ayrıntılar MQTT message expiry yazısında.
Dar subscribe olun
Bir test <kendi öneki>/# topic'ine ya da onun altındaki belirli topic'lere subscribe olmalıdır; daha genişine değil. Bir filtreye güvenmeden önce onu topic matcher ile beklediğiniz topic'lere karşı kontrol edin; yanlış yere konmuş bir +, bir testin başka bir testin mesajlarını almaya başlamasının klasik yoludur.
Kontrol ettiğiniz bir broker'da bunu ACL'lerle zorunlu kılın: CI kullanıcısına yalnızca testmqtt/# için okuma ve yazma izni verin. Yanlışlıkla bir üretim topic'ini hedefleyen bir test böylece publish etmek yerine "not authorized" hatasıyla başarısız olur.
Yarıda kalan çalıştırmaların artıklarını temizleyin
Bir CI job'ı iptal edildiğinde ya da bir dizüstü uyku moduna geçtiğinde teardown çalışmaz. Her run ID bir zaman damgasıyla başladığı için zamanlanmış bir temizlik job'ı bir günden eski çalıştırmaların retained mesajlarını silebilir:
import os, time
import paho.mqtt.client as mqtt
cutoff = time.strftime("%Y%m%dT%H%M%S", time.gmtime(time.time() - 86400))
stale = set()
def on_message(client, userdata, msg):
parts = msg.topic.split("/") # testmqtt/<suite>/<run-id>/...
if msg.retain and msg.payload and len(parts) > 3 and parts[2][:15] < cutoff:
stale.add(msg.topic)
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="testmqtt-janitor")
if os.getenv("MQTT_USER"):
c.username_pw_set(os.environ["MQTT_USER"], os.environ["MQTT_PASS"])
c.on_message = on_message
c.connect(os.getenv("MQTT_HOST", "broker.connectmq.com"), 1883)
c.loop_start()
c.subscribe("testmqtt/#", qos=1)
time.sleep(5)
for topic in stale:
c.publish(topic, b"", qos=1, retain=True).wait_for_publish(5)
print(f"cleared {len(stale)} stale retained messages")
c.loop_stop(); c.disconnect()Bunu her gece çalıştırın, örneğin GitHub Actions içindeki test pipeline'ınızın yanında zamanlanmış bir job olarak. Bir retained mesaj silinmek bilmiyorsa retained mesaj silinmiyor yazısı olağan nedenleri listeliyor.
Client ID'ler ve oturumlar
İki çalıştırma aynı client ID'yi paylaşıyorsa topic izolasyonu boşa gider. Her client ID'ye run ID'yi ekleyin (fixture'ın yaptığı gibi), test özellikle kalıcılıkla ilgili değilse temiz oturumla bağlanın (MQTT 5'te clean_start=True, 3.1.1'de clean_session=True) ve katı MQTT 3.1.1 broker'larını desteklemeniz gerekiyorsa ID'leri 23 karakterle sınırlayın. Client ID üreteci tutarlı formatta ID'ler üretir.
Kontrol listesi
- Bir testteki her topic bir önekten oluşturulur; koda gömülü topic yoktur.
- Önek, UTC zaman damgasıyla başlayan bir run ID içerir.
- Subscription'lar asla testin kendi önekinin üstüne çıkmaz.
- Teardown önek altındaki retained mesajları temizler; gece çalışan bir temizlik job'ı gerisini halleder.
- MQTT 5 kullanılabildiğinde retained test mesajları bir süre sonu taşır.
- Client ID'ler run ID'yi içerir; oturumlar varsayılan olarak temizdir.
Test topic'lerinizi tarayıcıda inceleyin
Test topic'lerini adlandırmak, bütün bir kararsız test sınıfını ortadan kaldıran beş dakikalık bir karardır. Her şeyin önüne benzersiz, zaman damgalı bir önek koyun, subscription'ları onun içinde tutun ve arkanızı temizleyin; böylece tek bir broker'a karşı istediğiniz kadar test paketini paralel çalıştırabilirsiniz.
Sıkça sorulan sorular
Paralel MQTT testlerinin birbirinin mesajlarını almasını nasıl önlerim?
Her test çalıştırmasına, ideal olarak her test senaryosuna, testmqtt/<suite>/<run-id>/<case> gibi kendi topic önekini verin ve yalnızca bu önekin altına subscribe olun. Bir testte asla # veya paylaşılan bir topic'e subscribe olmayın.
Testlerin oluşturduğu retained mesajları nasıl silerim?
Retained mesaj tutan her topic'e retain bayrağıyla boş bir payload publish edin. Bir teardown adımı test önekine subscribe olup retain bayrağıyla gelen topic'leri toplayabilir ve onları temizleyebilir.
Bir test çalıştırma ID'si nasıl olmalı?
Sıralanabilir bir UTC zaman damgasıyla başlayın, varsa CI çalıştırma numarasını ekleyin ve birkaç rastgele karakterle bitirin; örneğin 20261017T101500-gh812-a3f9. Zaman damgası, eski çalıştırmalardan kalanları silmeyi kolaylaştırır.

