Go Tests lernen von Kubernetes
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Warum Testen
Gutes Unit-Testing kann zu einem eleganteren Code-Design führen und dadurch die Verständlichkeit, Wiederverwendbarkeit und Wartbarkeit des Codes verbessern. Wenn Änderungen eingeführt werden, ist es nicht erforderlich, das gesamte Programm erneut zu testen – stellen Sie einfach sicher, dass die Ein- und Ausgaben der geänderten Teile konsistent bleiben, und Sie können schnell überprüfen, ob es Probleme mit dem Programm gibt.
Zusätzlich können wir bei jedem Auftreten eines Fehlers die Eingabe des Fehlers als Testfall hinzufügen. Auf diese Weise machen wir nicht noch einmal den gleichen Fehler, und wir müssen die Tests nur einmal pro Durchgang ausführen, um zu sehen, ob neue Änderungen ähnliche Probleme aus der Vergangenheit wieder eingeführt haben. Dies ist eine signifikante Verbesserung der Softwarequalität.
Methoden als Parameter übergeben, um Mocking zu erleichtern
In der Graceful-Shutdown-Logik von Kubernetes können wir, indem wir den Handler-Parameter als Methode deklarieren, anstatt ihn direkt aufzurufen, nur die Logik von flushList
testen, ohne uns um die Korrektheit des Handlers selbst kümmern zu müssen.
Wir können aber auch die Reflektion von gomonkey verwenden, um direkt den Rückgabewert einer Methode zu mocken, um den gleichen Effekt zu erzielen.
Wenn wir auf Race Conditions testen müssen, können wir dies tun, indem wir Goroutinen starten.
type gracefulTerminationManager struct { rsList graceTerminateRSList } func newGracefulTerminationManager() *gracefulTerminationManager { return &gracefulTerminationManager{ rsList: graceTerminateRSList{ list: make(map[string]*item), }, } } type item struct { VirtualServer string RealServer string } type graceTerminateRSList struct { lock sync.Mutex list map[string]*item } func (g *graceTerminateRSList) flushList(handler func(rsToDelete *item) (bool, error)) bool { g.lock.Lock() defer g.lock.Unlock() success := true for _, rs := range g.list { if ok, err := handler(rs); !ok || err != nil { success = false } } return success } func (g *graceTerminateRSList) add(rs *item) { g.lock.Lock() defer g.lock.Unlock() g.list[rs.RealServer] = rs } func (g *graceTerminateRSList) len() int { g.lock.Lock() defer g.lock.Unlock() return len(g.list) }
Hier müssen wir flushList
und add
unter Race Conditions testen.
func Test_raceGraceTerminateRSList_flushList(t *testing.T) { manager := newGracefulTerminationManager() go func() { for i := 0; i < 100; i++ { manager.rsList.add(&item{ VirtualServer: "virtualServer", RealServer: fmt.Sprint(i), }) } }() // Wait until a certain number of elements are added before proceeding for manager.rsList.len() < 20 { } // Pass in the handler for mocking success := manager.rsList.flushList(func(rsToDelete *item) (bool, error) { return true, nil }) assert.True(t, success) }
Indem Sie https://github.com/agiledragon/gomonkey verwenden, um Teile Ihres Programms zu mocken, können Sie die zu testenden Methoden von den Auswirkungen externer Aufrufe isolieren.
Wenn Sie private Methoden stubben müssen, können Sie eine höhere Version von gomonkey verwenden, sodass Sie sich mehr auf die zu testenden Methoden konzentrieren können.
Wenn Sie Integrationstests in Ihren Testdateien durchführen möchten, stoßen Sie möglicherweise auf ein Problem: Sie müssen zuerst viele Ressourcen initialisieren, z. B. Datenbanken und Caches. In diesem Fall können Sie Methoden zum Initialisieren dieser Ressourcen unter dem Verzeichnis jedes Moduls hinzufügen. Zum Beispiel:
func InitTestSuite(opts ...TestSuiteConfigOpt) { config := &TestSuiteConfig{} for _, opt := range opts { opt(config) } dsn := config.GetDSN() err := NewOrmClient(&Config{ Config: &gorm.Config{ //Logger: logger.Default.LogMode(logger.Info), }, SourceConfig: &SourceDBConfig{}, Dial: postgres.Open(dsn), }) }
Dann initialisieren Sie in der Testdatei, in der Sie sie verwenden müssen, über die Methode TestMain
.
Ein weiterer Vorteil ist, dass Sie frühzeitig erkennen können, ob Ihre Module sauber entkoppelt sind. Wenn Sie beispielsweise feststellen, dass Sie viele Komponenten initialisieren müssen, wenn Sie eine Testsuite einrichten, lohnt es sich zu überprüfen, ob Ihr Moduldesign korrekt oder erforderlich ist.
Wie manConcurrency-Probleme testet
Wie schreibt man Tests für Concurrent-Programme?
In verteilten Systemen ist das häufigste Problem eine große Anzahl von Race Conditions. Viele Fälle treten nur mit sehr geringer Wahrscheinlichkeit auf, aber wenn sie auftreten, können sie zu schwerwiegenden Unfällen führen. Daher müssen wir so viele konkurrierende Race-Szenarien wie möglich simulieren und die Ergebnisse überprüfen, nachdem alle Operationen abgeschlossen sind. Gelegentlich kann der Test jedoch in einem einzelnen Durchlauf erfolgreich bestanden werden, daher müssen wir sicherstellen, dass das Ergebnis auch nach mehreren Ausführungen konsistent ist. Dies erfordert die mehrmalige Ausführung des Codes, wie im folgenden Beispielcode gezeigt:
var ( counter int ) func increment() { counter++ } func TestIncrement(t *testing.T) { count := 100 var wg sync.WaitGroup for i := 0; i < count; i++ { wg.Add(1) go func() { increment() wg.Done() }() } assert.Equal(t, count, counter) }
Indem Sie mehrere Goroutinen starten, um die Methode zu bearbeiten, stellen Sie möglicherweise fest, dass die Ergebnisse nicht Ihren Erwartungen entsprechen. An diesem Punkt müssen Sie Ihren Code überprüfen und ändern.
TDD (Testgetriebene Entwicklung)
Jedes Mal, nachdem Sie einen Test geschrieben haben, schreiben Sie nur den minimalen Code, der erforderlich ist, um den Test zu bestehen. Nehmen Sie beispielsweise die Implementierung von State-Machine-Code. Definieren Sie zuerst Ihre Methoden. Für eine einfachere Lesbarkeit hier eine einfache Implementierung:
func GetOrder(orderId string) Order { return Order{} } func UpdateOrder(originalOrder, order Order) error { return nil } func UpdateOrderStateByEvent(ctx context.Context, orderId string, event Event) (err error) { order := GetOrder(orderId) stateMap, ok := orderEventStateMap[event] if !ok { return errors.New("event not exists") } if !stateMap.currentStateSet.Contains(order.OrderState) { return errors.New("current OrderState error") } updateOrder := Order{ OrderId: order.OrderId, OrderState: order.OrderState, } err = UpdateOrder(order, updateOrder) if err != nil { return err } return nil }
Testen Sie dann UpdateOrderStateByEvent
. Wir müssen uns darüber im Klaren sein, dass Unit-Tests dazu dienen, diese Methode isoliert zu testen. Andere Methoden können mit gomonkey gemockt werden, um die Wiederholbarkeit des Tests sicherzustellen.
func TestOrderStateByEvent(t *testing.T) { type args struct { ctx context.Context orderId string event Event } tests := []struct { name string args args wantErr error initStubs func() (reset func()) }{{ name: "", args: args{ ctx: context.Background(), orderId: "orderId1", event: onHoldEvent, }, wantErr: nil, initStubs: func() (reset func()) { patches := gomonkey.ApplyFunc(GetOrder, func(orderId string) Order { return Order{ OrderId: orderId, OrderState: delivering, } }) return func() { patches.Reset() } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 1. Mock the required methods reset := tt.initStubs() defer reset() // 2. Call the method to be tested err := UpdateOrderStateByEvent(tt.args.ctx, tt.args.orderId, tt.args.event) assert.Nil(t, err) }) } }
Das Konzept der testgetriebenen Entwicklung wurde bereits in den 1990er Jahren vorgeschlagen. Obwohl dieses Beispiel Go verwendet, wurde TDD zuerst in anderen Sprachen praktiziert. Der Autor schreibt Tests und dann den minimalen Code, der erforderlich ist, um die Tests zu bestehen, wobei er zwischen diesen Schritten wechselt. Wenn das Programm abgeschlossen ist, befindet es sich bereits in einem testbaren Zustand.
Indem Sie mit dem Testcode beginnen, können Sie vermeiden, zögerlich größere Änderungen vorzunehmen, nachdem Sie den eigentlichen Code geschrieben haben. Dies verhindert, dass Funktionen zu lang werden, was zukünftige Änderungen und erneute Tests wesentlich einfacher macht. Wenn Sie Geschäftslogik entwickeln, werden Sie auf weniger Fehler stoßen, wenn Sie die Geschäftslogik im Voraus zerlegen und dann Komponenten mit Klebecode kombinieren, als wenn Sie den gesamten Code auf einmal schreiben und anschließend testen.
Einige mögen denken, dass das Schreiben von Tests zu viel Zeit in Anspruch nimmt, aber wir können Tools verwenden, um unsere Testeffizienz zu verbessern. Verwenden Sie beispielsweise die IDE, um Testgerüste zu erstellen: Mit dem Aufkommen von KI-Copiloten müssen sich wiederholende Arbeiten in Testfällen nicht mehr manuell erledigt werden. Jetzt müssen Sie nur noch einen Fall schreiben und die Logik für die Testmethode implementieren, und KI kann helfen, viele Edge-Case-Beispiele zu generieren, manchmal sogar gründlicher, als Sie selbst denken würden. Darüber hinaus sind die generierten Beispiele äußerst brauchbar, solange Ihre Methodennamen gut gewählt sind. Wenn die von der KI generierten Testfälle nicht geeignet sind, können Sie darüber nachdenken, ob der Methodenname selbst problematisch ist, und Ihren Code kontinuierlich verbessern.
Fazit
Wir müssen nicht beim ersten Versuch eleganten Code schreiben, aber wir sollten immer danach streben, besseren Code zu schreiben, kontinuierlich über unsere Arbeit nachzudenken und Tools zu verwenden, um uns ständig zu verbessern. Auf diese Weise werden auch die Ergebnisse, die wir erzielen, herausragender.
Wir sind Leapcell, Ihre erste Wahl für das Hosting von Go-Projekten.
Leapcell ist die Next-Gen Serverless Plattform für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand - konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ