Einleitung: Warum Tests in Go wichtig sind
Go (Golang) ist eine Sprache, die von ihrem Design her auf Einfachheit, Leistung und Zuverlässigkeit ausgelegt ist. Das integrierte testing-Paket und leistungsstarke Tools wie go test machen das Schreiben von Tests zu einem integralen Bestandteil der Entwicklung. Ohne qualitativ hochwertige Tests wird selbst der eleganteste Code irgendwann zu einem "fragilen" System, bei dem eine einzige Änderung alles zum Einsturz bringt.
In diesem Artikel analysieren wir drei Schlüsselebenen des Testens: Unit, Integration und E2E (End-to-End). Sie erfahren, wie sie sich unterscheiden, wann sie anzuwenden sind und wie man effektive Tests in Go schreibt. Das Material ist sowohl für Anfänger als auch für erfahrene Entwickler nützlich, die ihr Wissen systematisieren möchten.
1. Unit-Tests
Unit-Tests sind die Grundlage der Testpyramide. Sie überprüfen die Funktionsweise einzelner Funktionen, Methoden oder Module isoliert von externen Abhängigkeiten (Datenbanken, Netzwerk, Dateisystem).
Grundprinzipien von Unit-Tests in Go
- Isolation: Der zu testende Code sollte nicht von externen Diensten abhängen.
- Geschwindigkeit: Unit-Tests werden in Millisekunden ausgeführt.
- Determinismus: Das Testergebnis sollte sich von einem Durchlauf zum nächsten nicht ändern.
Beispiel eines einfachen Unit-Tests
Angenommen, wir haben eine Funktion zum Addieren zweier Zahlen:
// math.gopackage math
func Add(a, b int) int { return a + b}Der Test für diese Funktion würde wie folgt aussehen:
// math_test.gopackage math
import "testing"
func TestAdd(t *testing.T) { result := Add(2, 3) expected := 5
if result != expected { t.Errorf("Add(2, 3) = %d; want %d", result, expected) }}Der Test wird mit dem Befehl ausgeführt: go test ./...
Tabellengetriebene Tests
Die Go-Community bevorzugt tabellengetriebene Tests zum Überprüfen mehrerer Fälle:
func TestAddTable(t *testing.T) { tests := []struct { a, b, expected int }{ {1, 1, 2}, {0, 0, 0}, {-1, 1, 0}, {100, 200, 300}, }
for _, tt := range tests { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }}Tipp: Verwenden Sie t.Run(), um verschachtelte Tests mit aussagekräftigen Namen zu erstellen – das verbessert die Lesbarkeit der Berichte.
2. Integrationstests
Integrationstests überprüfen die Interaktion zwischen Systemkomponenten: Datenbank, API, externe Dienste. Im Gegensatz zu Unit-Tests werden hier reale (oder nahezu reale) Abhängigkeiten eingesetzt.
Besonderheiten von Integrationstests in Go
- Erfordern oft eine Umgebungseinrichtung (z. B. eine Testdatenbank).
- Können Build-Tags verwenden, um Unit- und Integrationstests zu trennen.
- Laufen langsamer und werden daher separat ausgeführt.
Beispiel: Testen von Datenbankoperationen (PostgreSQL)
Erstellen wir eine Datei mit dem Build-Tag integration:
// db_test.go//go:build integration
package db
import ( "database/sql" "testing")
func TestSaveUser(t *testing.T) { // Verbindung zur Testdatenbank herstellen dsn := "host=localhost user=test password=test dbname=testdb sslmode=disable" db, err := sql.Open("postgres", dsn) if err != nil { t.Fatalf("failed to connect to db: %v", err) } defer db.Close()
// Daten vor dem Test bereinigen _, _ = db.Exec("TRUNCATE TABLE users")
// Die zu testende Operation ausführen user := User{Name: "Alice", Email: "alice@example.com"}