Aufbau robuster Go-Anwendungen mit TDD unter Nutzung von `testing` und `testify`
Min-jun Kim
Dev Intern · Leapcell

Einleitung
In der schnelllebigen Welt der Softwareentwicklung ist der Aufbau zuverlässiger und wartbarer Anwendungen von größter Bedeutung. Die Vernachlässigung ordnungsgemäßer Tests kann zu einer Kaskade von Problemen führen, von subtilen Fehlern in der Produktion bis hin zu prohibitiven Refactoring-Kosten. Test-Driven Development (TDD) Bietet eine leistungsstarke Methodik zur Minderung dieser Risiken, indem das Testparadigma in den Vordergrund der Entwicklung gerückt wird. Anstatt Tests zu schreiben, nachdem der Code abgeschlossen ist, plädiert TDD dafür, fehlschlagende Tests vor dem Schreiben von Produktionscode zu schreiben. Dieser Ansatz stellt nicht nur eine umfassende Testabdeckung sicher, sondern fungiert auch als Designwerkzeug, das den Entwicklungsprozess zu saubererem, modularerem und letztendlich qualitativ hochwertigerem Code führt. Dieser Artikel wird sich mit der praktischen Anwendung von TDD in Go befassen und demonstrieren, wie das integrierte testing
-Paket von Go zusammen mit der beliebten testify
-Assertionsbibliothek effektiv genutzt wird, um robuste und widerstandsfähige Anwendungen zu erstellen.
TDD und seine Werkzeuge verstehen
Bevor wir uns mit praktischen Beispielen beschäftigen, ist es wichtig, die Kernprinzipien von TDD und die Werkzeuge, die wir verwenden werden, zu verstehen.
Was ist TDD?
TDD folgt einem einfachen, aber tiefgründigen Dreischrittzyklus, der oft als "Rot, Grün, Refaktor" bezeichnet wird:
- Rot: Schreiben Sie einen fehlschlagenden Test für eine neue Funktion oder Funktionalität. Dieser Test sollte das gewünschte Verhalten klar definieren und zunächst fehlschlagen, da der entsprechende Produktionscode noch nicht existiert.
- Grün: Schreiben Sie gerade genug Produktionscode, um den fehlschlagenden Test zu bestehen. Das Ziel hier ist rein, den Test zu erfüllen, nicht unbedingt, perfekten oder optimierten Code zu schreiben.
- Refaktor: Sobald der Test bestanden ist, refaktorieren Sie den Produktionscode, um sein Design, seine Lesbarkeit und seine Wartbarkeit zu verbessern, ohne sein externes Verhalten zu ändern. In dieser Phase sollten alle vorhandenen Tests weiterhin erfolgreich sein und ein Sicherheitsnetz bieten.
Dieser iterative Prozess hilft Entwicklern, sich auf kleine, überschaubare Funktionsblöcke zu konzentrieren und sicherzustellen, dass jedes Teil ordnungsgemäß funktioniert, bevor sie fortfahren.
Go's integriertes Testing-Paket
Go wird mit einem leistungsstarken und gut integrierten testing
-Paket geliefert, das als Grundlage für das Schreiben von Unit-, Integrations- und sogar End-to-End-Tests dient. Hauptmerkmale sind:
- Testfunktionen: Testfunktionen werden identifiziert, indem sie mit
Test
gefolgt von einem Großbuchstaben beginnen (z. B.TestMyFunction
). Sie nehmen ein einzelnes Argument vom Typ*testing.T
an. - Ausführen von Tests: Tests werden mit dem Befehl
go test
ausgeführt. - Untertests: Der Typ
testing.T
ermöglicht die Erstellung von Untertests mitt.Run()
, was zur Organisation von Tests beiträgt und eine bessere Berichterstattung ermöglicht. - Assertions: Das
testing
-Paket bietet grundlegende Assertionsmethoden wiet.Error()
,t.Errorf()
,t.Fatal()
undt.Fatalf()
, um Testfehler anzuzeigen.
Testify Assertion Library
Während das testing
-Paket von Go hervorragend für die Strukturierung von Tests geeignet ist, sind seine integrierten Assertionsmechanismen zwar etwas umständlich. Hier kommt testify
ins Spiel. testify
ist ein beliebtes Drittanbieter-Assertion-Toolkit, das eine reichhaltige Sammlung ausdrucksstarker und lesbarer Assertionsfunktionen bietet, die Tests sauberer und leichter verständlich machen. Das am häufigsten verwendete Modul innerhalb von testify
ist assert
, das Funktionen wie assert.Equal()
, assert.NotNil()
, assert.True()
und viele mehr anbietet.
TDD in der Praxis: Aufbau eines E-Commerce-Bestellverarbeiters
Lassen Sie uns TDD veranschaulichen, indem wir eine vereinfachte Bestellverarbeitungslogik für eine E-Commerce-Anwendung erstellen. Wir beginnen mit einer Funktion, die den Gesamtpreis einer Bestellung berechnet.
Zuerst definieren wir unsere Order
- und LineItem
-Strukturen. Wir werden diese in einer Datei namens order.go
platzieren.
package order type LineItem struct { ProductID string Quantity int UnitPrice float64 } type Order struct { ID string LineItems []LineItem Discount float64 // Als Prozentsatz, z. B. 0,10 für 10% IsExpedited bool }
Schritt 1: Rot - Schreiben Sie einen fehlschlagenden Test
Unsere erste Anforderung ist die Berechnung des Gesamtpreises einer Bestellung ohne Rabatte. Wir erstellen eine Testdatei order_test.go
und schreiben einen Test, der einen bestimmten Gesamtbetrag erwartet.
package order_test import ( "testing" "github.com/stretchr/testify/assert" // testify assert-Paket importieren "your_module_path/order" // Ersetzen Sie dies durch Ihren Modulpfad ) func TestCalculateTotalPrice_NoDiscount(t *testing.T) { // Anordnen: Richten Sie die Testdaten ein items := []order.LineItem{ {ProductID: "P001", Quantity: 2, UnitPrice: 10.0}, {ProductID: "P002", Quantity: 1, UnitPrice: 25.0}, } testOrder := order.Order{ ID: "ORD001", LineItems: items, Discount: 0.0, } expectedTotal := 45.0 // (2 * 10.0) + (1 * 25.0) // Ausführen: Rufen Sie die Funktion auf, die wir implementieren möchten actualTotal := testOrder.CalculateTotalPrice() // Diese Funktion existiert noch nicht! // Assert: Überprüfen Sie, ob das tatsächliche Ergebnis dem erwarteten Ergebnis entspricht assert.Equal(t, expectedTotal, actualTotal, "Der Gesamtpreis sollte ohne Rabatt korrekt berechnet werden") }
Wenn Sie jetzt versuchen, go test
auszuführen, wird es fehlschlagen, da CalculateTotalPrice
nicht auf der Order
-Struktur existiert. Dies ist unser "roter" Zustand.
Schritt 2: Grün - Schreiben Sie Produktionscode, um den Test zu bestehen
Nun implementieren wir die CalculateTotalPrice
-Methode in order.go
gerade so weit, dass der Test besteht.
package order func (o *Order) CalculateTotalPrice() float64 { var total float64 for _, item := range o.LineItems { total += float64(item.Quantity) * item.UnitPrice } return total }
Führen Sie go test
erneut aus. Der Test TestCalculateTotalPrice_NoDiscount
sollte nun erfolgreich sein. Dies ist unser "grüner" Zustand.
Schritt 3: Refaktor - Code verbessern (Optional für diesen einfachen Fall jetzt)
Für diese sehr einfache Funktion gibt es in diesem Stadium nicht viel zu refaktorieren. Wenn jedoch die Komplexität steigt, wird dieser Schritt entscheidend für die Aufrechterhaltung der Codequalität. Wir könnten beispielsweise die Berechnung des Zeilenelement-Gesamtpreises in eine eigene Methode extrahieren, wenn die Logik für Zeilenelemente komplexer wird.
Erweiterung der Funktionalität: Rabatte anwenden
Lassen Sie uns nun die Funktion zum Anwenden eines Rabatts hinzufügen.
Rot: Schreiben Sie einen fehlschlagenden Test für Rabatt
func TestCalculateTotalPrice_WithDiscount(t *testing.T) { items := []order.LineItem{ {ProductID: "P001", Quantity: 2, UnitPrice: 10.0}, // 20.0 {ProductID: "P002", Quantity: 1, UnitPrice: 25.0}, // 25.0 } testOrder := order.Order{ ID: "ORD002", LineItems: items, Discount: 0.10, // 10% Rabatt } // Basisgesamtbetrag = 45.0 expectedTotal := 45.0 * (1 - 0.10) // 40.5 actualTotal := testOrder.CalculateTotalPrice() assert.InDelta(t, expectedTotal, actualTotal, 0.001, "Der Gesamtpreis sollte mit Rabatt korrekt berechnet werden") }
Wir verwenden assert.InDelta
für den Vergleich von Fließkommazahlen, um mögliche Ungenauigkeiten bei Fließkommazahlen zu berücksichtigen. Die Ausführung von go test
zeigt, dass TestCalculateTotalPrice_WithDiscount
fehlschlägt, da unsere aktuelle CalculateTotalPrice
-Methode den Rabatt nicht anwendet.
Grün: Implementieren Sie die Rabattlogik
Modifizieren Sie CalculateTotalPrice
in order.go
, um den Rabatt einzubeziehen.
package order func (o *Order) CalculateTotalPrice() float64 { var total float64 for _, item := range o.LineItems { total += float64(item.Quantity) * item.UnitPrice } // Rabatt anwenden total *= (1 - o.Discount) // Stellt sicher, dass der Rabatt ein Prozentsatz ist return total }
Führen Sie go test
aus. Sowohl TestCalculateTotalPrice_NoDiscount
als auch TestCalculateTotalPrice_WithDiscount
sollten nun erfolgreich sein.
Refaktor: Randfälle und Lesbarkeit berücksichtigen
Was passiert, wenn o.Discount
negativ oder größer als 1 ist? Obwohl unsere aktuellen Tests dies nicht abdecken, ermutigt TDD dazu, während des Refactorings oder während des nächsten Zyklus über solche Randfälle nachzudenken. Vorerst gehen wir von gültigen Rabattprozenten aus. Wir könnten während der Auftragserstellung eine Validierung für Discount
hinzufügen oder sie innerhalb von CalculateTotalPrice
behandeln.
Komplexere Szenarien: Zuschlag für beschleunigte Lieferung
Nehmen wir an, beschleunigte Aufträge haben einen festen Zuschlag.
Rot: Test für beschleunigten Zuschlag schreiben
func TestCalculateTotalPrice_ExpeditedShipping(t *testing.T) { items := []order.LineItem{ {ProductID: "P003", Quantity: 1, UnitPrice: 50.0}, } testOrder := order.Order{ ID: "ORD003", LineItems: items, Discount: 0.0, IsExpedited: true, } const expeditedSurcharge = 15.0 // Definieren wir eine Konstante dafür expectedTotal := 50.0 + expeditedSurcharge actualTotal := testOrder.CalculateTotalPrice() assert.Equal(t, expectedTotal, actualTotal, "Der Gesamtpreis sollte den Zuschlag für beschleunigte Lieferung enthalten") }
Dieser Test schlägt fehl, da die Logik für den Zuschlag nicht implementiert ist.
Grün: Zuschlagslogik hinzufügen
Fügen Sie den Zuschlag in order.go
hinzu. Wir definieren expeditedSurcharge
als Konstante auf Paketebene.
package order // expeditedSurcharge ist feste Kosten für die beschleunigte Lieferung const expeditedSurcharge float64 = 15.0 // LineItem ... (bestehender Code) type LineItem struct { // ... } // Order ... (bestehender Code) type Order struct { // ... } func (o *Order) CalculateTotalPrice() float64 { var total float64 for _, item := range o.LineItems { total += float64(item.Quantity) * item.UnitPrice } total *= (1 - o.Discount) if o.IsExpedited { total += expeditedSurcharge } return total }
Alle Tests sollten nun erfolgreich sein.
Refaktor: Mehrere Bedingungen und Untertests kombinieren
Wenn die CalculateTotalPrice
-Funktion wächst, ist es von Vorteil, die Untertestfunktion von Go zu nutzen, um Tests besser zu organisieren und Kombinationen von Bedingungen zu testen.
// Fügen Sie diese Hilfskonstante für bessere Lesbarkeit hinzu const expeditedSurcharge = 15.0 func TestCalculateTotalPrice(t *testing.T) { // Definieren Sie eine Liste von Testfällen tests := []struct { name string order order.Order expected float64 }{ { name: "KeinRabatt_RegulaereLieferung", order: order.Order{ LineItems: []order.LineItem{ {ProductID: "P001", Quantity: 2, UnitPrice: 10.0}, {ProductID: "P002", Quantity: 1, UnitPrice: 25.0}, }, Discount: 0.0, }, expected: 45.0, }, { name: "MitRabatt_RegulaereLieferung", order: order.Order{ LineItems: []order.LineItem{ {ProductID: "P001", Quantity: 2, UnitPrice: 10.0}, {ProductID: "P002", Quantity: 1, UnitPrice: 25.0}, }, Discount: 0.10, // 10% }, expected: 40.5, // 45 * 0,9 }, { name: "KeinRabatt_BeschleunigteLieferung", order: order.Order{ LineItems: []order.LineItem{ {ProductID: "P003", Quantity: 1, UnitPrice: 50.0}, }, Discount: 0.0, IsExpedited: true, }, expected: 50.0 + expeditedSurcharge, }, { name: "MitRabatt_BeschleunigteLieferung", order: order.Order{ LineItems: []order.LineItem{ {ProductID: "P004", Quantity: 3, UnitPrice: 10.0}, // 30.0 {ProductID: "P005", Quantity: 1, UnitPrice: 20.0}, // 20.0 }, // Basis 50.0 Discount: 0.20, // 20% IsExpedited: true, }, expected: (50.0 * (1 - 0.20)) + expeditedSurcharge, // 40,0 + 15,0 = 55,0 }, { name: "LeererAuftrag", order: order.Order{ LineItems: []order.LineItem{}, Discount: 0.0, IsExpedited: false, }, expected: 0.0, }, { name: "LeererAuftrag_Beschleunigt", order: order.Order{ LineItems: []order.LineItem{}, Discount: 0.0, IsExpedited: true, }, expected: expeditedSurcharge, // Zuschlag gilt auch dann, wenn der Gesamtbetrag 0 ist }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { act := tc.order.CalculateTotalPrice() assert.InDelta(t, tc.expected, actual, 0.001, "Der Gesamtpreis stimmte für den Testfall nicht überein: %s", tc.name) }) } }
Diese Refaktorierung ersetzt unsere einzelnen Testfunktionen durch einen einzigen TestCalculateTotalPrice
, der tabellengesteuerte Tests und Untertests verwendet. Dies macht die Tests organisierter, erleichtert das Hinzufügen neuer Fälle und vermeidet Wiederholungen (Don't Repeat Yourself). Die Ausgabe von go test
zeigt deutlich die Ergebnisse jedes Untertests.
Fazit
TDD führt bei sorgfältiger Anwendung mit dem testing
-Paket von Go und testify
zu zuverlässigeren, wartbareren und gut gestalteten Go-Anwendungen. Durch das Schreiben von Tests zuerst werden Entwickler ermutigt, sorgfältig über das API-Design, Randfälle und das allgemeine Verhalten ihres Codes vor der Implementierung nachzudenken. Dieser disziplinierte Ansatz fängt Fehler frühzeitig ab, dient aber auch als gelebte Dokumentation, die Klarheit darüber gibt, wie jede Komponente der Anwendung funktionieren soll. Er fördert eine Kultur der Qualität, macht zukünftiges Refactoring sicherer und Feature-Hinzufügungen robuster. Die Implementierung von TDD mit testing
und testify
in Go ist ein einfacher, aber leistungsstarker Weg, um die Qualität Ihrer Softwareentwicklung zu verbessern.