Broker testleri

Go'da MQTT Client'larını Test Etmek

Go MQTT client'larını go test ile test edin: paho.mqtt.golang ve autopaho helper'ları, mesaj doğrulama için channel'lar, tablo tabanlı QoS testleri ve fake'ler.

Güncellendi · 9 dk okuma

Go'da MQTT Client'larını Test Etmek

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 testHayır — sahte bir publisherHandler mantığı, payload kodlama, topic oluşturma
Entegrasyon testiEvet — gerçek brokerBağ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.

mqtt_helpers_test.go
//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:

  • Wait yerine WaitTimeout. Broker hiç yanıt vermezse Wait() sonsuza kadar bloklar; go test takı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. Subscribe bir 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:

roundtrip_test.go
//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:

Run integration tests
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:

expect_none_test.go
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.

autopaho_test.go
//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:

publisher.go and publisher_test.go
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.