Broker testleri

Testlerin Asla Çakışmaması İçin Test Topic'lerini Adlandırma

MQTT testlerinin çakışmasını önleyin: çalıştırma başına benzersiz topic önekleri, CI run ID'leri, dar wildcard'lar, tekil client ID ve retained temizliği.

Güncellendi · 8 dk okuma

Testlerin Asla Çakışmaması İçin Test Topic'lerini Adlandırma

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ÖrnekAmaç
KöktestmqttTüm test trafiğini gerçek trafikten ayırır; ACL ile kısıtlaması kolaydır
SuiteordersTopic'in hangi test paketine veya servise ait olduğu
Run ID20261017T101500-gh812-a3f9Her test çalıştırması için tek bir değer, makineler arasında benzersiz
Senaryotest-retained-statusİsteğe bağlı: her test senaryosu için bir alt ağaç
Mantıksal topicdevice/42/statusKodunuzun ü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ÖrnekArtılarıEksileri
Rastgele UUID5f0c9e2a…Her yerde benzersizSıralanamaz, loglarda okunması zor
CI çalıştırma numarasıgh812-1Topic'leri bir pipeline çalıştırmasına bağlarDizüstü bilgisayarlarda yoktur; repolar arasında benzersiz değildir
Zaman damgası + rastgele20261017T101500-a3f9Sıralanabilir, okunur, her yerde çalışırBiraz 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'ı

conftest.py
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

test-topics.mjs
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:

güvenlik ağıyla retained publish
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:

janitor.py
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

  1. Bir testteki her topic bir önekten oluşturulur; koda gömülü topic yoktur.
  2. Önek, UTC zaman damgasıyla başlayan bir run ID içerir.
  3. Subscription'lar asla testin kendi önekinin üstüne çıkmaz.
  4. Teardown önek altındaki retained mesajları temizler; gece çalışan bir temizlik job'ı gerisini halleder.
  5. MQTT 5 kullanılabildiğinde retained test mesajları bir süre sonu taşır.
  6. 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.