Go'da MQTT client'larını test etmek en iyi sade go test ile işler: bir client'ı bağlayan küçük bir helper, mesajları alan buffer'lı bir channel ve "mesaj hiç gelmedi" durumunu temiz bir test hatasına dönüştüren timeout'lu bir select. Bu rehber bu kalıbı paho.mqtt.golang (MQTT 3.1.1) ve autopaho (MQTT 5) için gösteriyor; ayrıca tablo tabanlı testleri ve hiç broker gerektirmemesi gereken kod için fake'leri ele alıyor.
İki tür test
| Test türü | Broker ile konuşur mu? | Amaç |
|---|---|---|
| Unit test | Hayır — sahte bir publisher | Handler mantığı, payload kodlama, topic oluşturma |
| Entegrasyon testi | Evet — gerçek broker | Bağlantı seçenekleri, TLS, kimlik doğrulama, QoS, retained mesajlar, ACL'ler |
Somut client yerine küçük bir interface'e bağımlı olarak mantığın çoğunu MQTT olmadan test edilebilir tutun ve broker'ı yalnızca bir broker'ın kanıtlayabileceği şeylere ayırın. Rehberin geri kalanı entegrasyon testleriyle ilgili; fake'ler sonda geliyor.
paho.mqtt.golang için bir test helper'ı
Kütüphaneyi daha önce kullanmadıysanız Paho ile Go'da MQTT temelleri anlatıyor. Testlerde bağlantı kurulumunu t.Helper() çağıran ve temizliği kaydeden bir helper'a sarın; böylece her test üç satır olur.
//go:build integration
package telemetry
import (
"crypto/rand"
"encoding/hex"
"os"
"testing"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
func brokerURL() string {
if u := os.Getenv("MQTT_URL"); u != "" {
return u
}
return "tcp://broker.connectmq.com:1883"
}
func randomID(t *testing.T) string {
t.Helper()
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
t.Fatal(err)
}
return hex.EncodeToString(b)
}
// newClient connects a client and disconnects it when the test ends.
func newClient(t *testing.T) mqtt.Client {
t.Helper()
opts := mqtt.NewClientOptions().
AddBroker(brokerURL()).
SetClientID("gotest-" + randomID(t)).
SetUsername(os.Getenv("MQTT_USER")).
SetPassword(os.Getenv("MQTT_PASS")).
SetCleanSession(true).
SetConnectTimeout(10 * time.Second)
c := mqtt.NewClient(opts)
tok := c.Connect()
if !tok.WaitTimeout(15 * time.Second) {
t.Fatal("connect: timed out")
}
if err := tok.Error(); err != nil {
t.Fatalf("connect: %v", err)
}
t.Cleanup(func() { c.Disconnect(250) })
return c
}
// subscribe returns a buffered channel that receives every message on filter.
func subscribe(t *testing.T, c mqtt.Client, filter string, qos byte) <-chan mqtt.Message {
t.Helper()
ch := make(chan mqtt.Message, 100)
tok := c.Subscribe(filter, qos, func(_ mqtt.Client, m mqtt.Message) { ch <- m })
if !tok.WaitTimeout(5*time.Second) || tok.Error() != nil {
t.Fatalf("subscribe %s: %v", filter, tok.Error())
}
return ch
}
// expectMessage fails the test unless a message with payload want arrives in time.
func expectMessage(t *testing.T, ch <-chan mqtt.Message, want string, timeout time.Duration) mqtt.Message {
t.Helper()
select {
case m := <-ch:
if got := string(m.Payload()); got != want {
t.Fatalf("topic %s: got payload %q, want %q", m.Topic(), got, want)
}
return m
case <-time.After(timeout):
t.Fatalf("no message within %s", timeout)
return nil
}
}Üç ayrıntı en yaygın takılmaları önler:
WaityerineWaitTimeout. Broker hiç yanıt vermezseWait()sonsuza kadar bloklar;go testtakılan bir testi ancak varsayılan 10 dakikalık timeout sonunda öldürür.- Buffer'lı bir channel. paho varsayılan olarak mesajları sırayla teslim eder; dolu bir channel'da bloklanan bir handler tüm client'ı durdurur — publish'inizin beklediği onaylar dahil.
- SUBACK'i beklemek.
Subscribebir token döndürür; publish etmeden önce onu bekleyin, yoksa ilk mesaj abonelikten önce geçip gidebilir.
Tablo tabanlı bir gidiş-dönüş testi
Helper'lar hazır olunca, her QoS seviyesinde teslimatı kontrol eden bir test kısa ve okunur olur:
//go:build integration
package telemetry
import (
"testing"
"time"
)
func TestRoundTrip(t *testing.T) {
tests := []struct {
name string
qos byte
payload string
}{
{"qos0 small", 0, "21.5"},
{"qos1 json", 1, `{"temp":21.5,"unit":"C"}`},
{"qos2 text", 2, "door=open"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
c := newClient(t)
topic := "testmqtt/go/" + randomID(t) + "/temperature"
msgs := subscribe(t, c, topic, tc.qos)
tok := c.Publish(topic, tc.qos, false, tc.payload)
if !tok.WaitTimeout(5*time.Second) || tok.Error() != nil {
t.Fatalf("publish: %v", tok.Error())
}
m := expectMessage(t, msgs, tc.payload, 5*time.Second)
if m.Qos() != tc.qos {
t.Errorf("delivered at QoS %d, want %d", m.Qos(), tc.qos)
}
})
}
}Her subtest yeni bir client ve topic alır, bu yüzden t.Parallel() güvenlidir. Teslim edilen QoS'un, publish ve abonelik QoS'larının minimumu olduğunu unutmayın — buradaki bir uyuşmazlık genellikle broker'ın aboneliği düşürdüğü anlamına gelir. Ayrıntılar için QoS teslimatını test etmek yazısına bakın.
Suite'i race detector ile ve test önbelleği olmadan çalıştırın; önbellekten gelen bir sonuç bugünkü broker hakkında hiçbir şey söylemez:
export MQTT_URL=tcp://broker.connectmq.com:1883
go test -tags integration -race -count=1 -timeout 2m ./...Sessizliği doğrulamak: ACL ve filtre testleri
Bazı testler bir mesajın gelmediğini kanıtlamalıdır — eşleşmemesi gereken bir wildcard ya da client'ın okuyamayacağı bir topic. İkinci bir helper ekleyin:
func expectNoMessage(t *testing.T, ch <-chan mqtt.Message, wait time.Duration) {
t.Helper()
select {
case m := <-ch:
t.Fatalf("unexpected message on %s: %q", m.Topic(), m.Payload())
case <-time.After(wait):
}
}wait süresini ölçülen gecikmeye göre seçin: pozitif bir gidiş-dönüş normalde 50 ms sürüyorsa iki saniyelik sessizlik güçlü bir kanıttır. Hangi sonucu beklemeniz gerektiğini bilmek için filtre mantığınızı önce topic matcher ile kontrol edin.
Aynı kalıp autopaho ile (MQTT 5)
github.com/eclipse/paho.golang içindeki autopaho yeniden bağlanmayı yönetir ve MQTT 5 konuşur. Mesajlar OnPublishReceived callback'leri üzerinden gelir; onları önceki gibi bir channel'a aktarın.
//go:build integration
package telemetry
import (
"context"
"net/url"
"testing"
"time"
"github.com/eclipse/paho.golang/autopaho"
"github.com/eclipse/paho.golang/paho"
)
func TestAutopahoRoundTrip(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
u, _ := url.Parse("mqtt://broker.connectmq.com:1883")
msgs := make(chan *paho.Publish, 10)
topic := "testmqtt/go/" + randomID(t) + "/v5"
cm, err := autopaho.NewConnection(ctx, autopaho.ClientConfig{
ServerUrls: []*url.URL{u},
KeepAlive: 20,
CleanStartOnInitialConnection: true,
ClientConfig: paho.ClientConfig{
ClientID: "gotest-v5-" + randomID(t),
OnPublishReceived: []func(paho.PublishReceived) (bool, error){
func(pr paho.PublishReceived) (bool, error) {
msgs <- pr.Packet
return true, nil
},
},
},
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = cm.Disconnect(context.Background()) })
if err := cm.AwaitConnection(ctx); err != nil {
t.Fatalf("connect: %v", err)
}
if _, err := cm.Subscribe(ctx, &paho.Subscribe{
Subscriptions: []paho.SubscribeOptions{{Topic: topic, QoS: 1}},
}); err != nil {
t.Fatalf("subscribe: %v", err)
}
if _, err := cm.Publish(ctx, &paho.Publish{
Topic: topic, QoS: 1, Payload: []byte("hello v5"),
Properties: &paho.PublishProperties{
User: paho.UserProperties{{Key: "test", Value: t.Name()}},
},
}); err != nil {
t.Fatalf("publish: %v", err)
}
select {
case p := <-msgs:
if string(p.Payload) != "hello v5" {
t.Fatalf("got %q", p.Payload)
}
if p.Properties.User.Get("test") != t.Name() {
t.Errorf("user property lost")
}
case <-ctx.Done():
t.Fatal("no message before deadline")
}
}Bir user property üzerinde doğrulama yapmak, MQTT 5'in uçtan uca müzakere edildiğini kontrol etmenin ucuz bir yoludur — sessizce 3.1.1'e düşen bir broker veya proxy bu özelliği atardı. autopaho'nun API'si minor sürümler arasında değişti; modül sürümünü go.mod içinde sabitleyin ve güncellerken sürüm notlarını okuyun.
Broker'sız unit testler
Yalnızca publish eden kod, unit testlerde nadiren gerçek bir broker'a ihtiyaç duyar. Bir interface'e bağımlı olun ve yerine bir fake koyun:
type Publisher interface {
Publish(topic string, qos byte, retained bool, payload interface{}) mqtt.Token
}
// In the test file:
type sentMsg struct {
topic string
payload interface{}
}
type fakePublisher struct{ sent []sentMsg }
func (f *fakePublisher) Publish(topic string, _ byte, _ bool, p interface{}) mqtt.Token {
f.sent = append(f.sent, sentMsg{topic, p})
return &mqtt.DummyToken{}
}mqtt.Client bu interface'i zaten karşılar; production kodu gerçek client'ı, testler ise fake'i geçirir. mqtt.DummyToken, hatasız şekilde anında tamamlanan hazır bir token'dır. Broker semantiğine bağlı davranışlar için — retained mesajlar, wildcard eşleşmesi, oturum durumu — gömülü bir broker veya mock broker daha iyi bir araçtır; bkz. unit testler için mock broker'lar.
Entegrasyon testleri nereyi hedeflemeli
Lokalde hızlı çalıştırmalar için bir Mosquitto container'ı veya paylaşılan broker.connectmq.com endpoint'i yeterlidir. CI'da ise production'a benzeyen bir broker istersiniz: aynı TLS portu (paho için ssl://...:8883, autopaho için mqtts://), gerçek kimlik bilgileri ve cihazlarınızın tabi olduğu ACL'ler. GitHub Actions'ta MQTT testleri bunları secret olarak nasıl enjekte edeceğinizi gösteriyor.
Bir test başarısız olduğunda ve nedenini anlayamadığınızda, testi -run ve -v ile çalıştırın ve aynı anda online MQTT client üzerinde testmqtt/go/# topic'ini izleyin. Mesaj orada görünüp testinizde görünmüyorsa sorun aboneliğinizde veya handler'ınızdadır; hiç görünmüyorsa publish tarafına bakın — Go Paho hata rehberi olağan nedenleri listeliyor.
go test trafiğini tarayıcıda izleyin
Asenkronluk kontrol altına alındığında Go, MQTT testlerini keyifli hale getirir: t.Fatalf ile hızlı başarısız olan helper'lar, buffer'lı channel'lar, her beklenti için timeout'lu bir select ve her subtest için benzersiz bir client ID ile topic. Broker gerektirmeyen mantığı bir interface'in arkasında tutun, gerçek gidiş-dönüşleri bir build tag'in arkasında çalıştırın; aynı suite dizüstü bilgisayarınızı, CI'ı ve deploy sonrası bir smoke testi kapsasın.
Sıkça sorulan sorular
Go MQTT testim neden sonsuza kadar asılı kalıyor?
Genellikle bir token Wait() ile bekleniyordur ve broker hiç yanıt vermez ya da bir mesaj handler'ı buffer'sız bir channel'da bloklanıp client'ın router'ını durdurur. WaitTimeout kullanın, mesaj channel'larına buffer verin ve onları her zaman timeout'lu bir select ile okuyun.
Go MQTT testleri t.Parallel kullanmalı mı?
Evet, her test kendi client ID'sini ve topic önekini kullandığı sürece. Entegrasyon testleri zamanlarının çoğunu ağı bekleyerek geçirir; paralel çalıştırmak suite'i ciddi biçimde kısaltır.
Unit test çalıştırmalarında MQTT entegrasyon testlerini nasıl atlarım?
Onları //go:build integration gibi bir build tag'in arkasına koyup go test -tags integration ile çalıştırın ya da testing.Short() true olduğunda veya MQTT_URL ortam değişkeni tanımlı değilse t.Skip çağırın.


