Einleitung: Warum fortgeschrittenes Testen in Go ein Must-have ist
Go (Golang) ist bekannt für seine Einfachheit und die integrierten Testwerkzeuge. Wenn ein Projekt jedoch über "Hello, World" hinauswächst, reicht der standardmäßige go test nicht mehr aus. Auf fortgeschrittenem Niveau stehen Entwickler vor Fragen: Wie isoliert man Tests richtig, wie testet man die Arbeit mit einer Datenbank, wie imitiert man externe Dienste und wie organisiert man eine End-to-End-Überprüfung der gesamten Anwendung?
In diesem Artikel behandeln wir drei Testebenen – Unit, Integration und E2E – anhand von Beispielen aus der Praxis. Sie erfahren, wie Sie Mocks, Container für Integrationstests verwenden und eine zuverlässige Pipeline für CI/CD aufbauen. Das Material richtet sich an Entwickler, die bereits mit den Grundlagen von Go vertraut sind und die Qualität ihres Codes auf die nächste Stufe heben möchten.
1. Unit-Tests: Isolation, Mocks und das Table-Driven-Tests-Pattern
Unit-Tests überprüfen die Arbeit eines einzelnen Moduls (Funktion, Methode) in vollständiger Isolation von externen Abhängigkeiten. In Go werden dafür häufig Interfaces und Mock-Bibliotheken wie testify/mock oder gomock verwendet.
1.1. Interfaces als Grundlage des Testens
Um einen echten Dienst durch einen Mock zu ersetzen, muss der Code von einem Interface abhängen, nicht von einer konkreten Implementierung. Betrachten wir ein Beispiel für einen Dienst zum Senden von Benachrichtigungen.
// notifier.gotype Notifier interface { Send(to string, message string) error}
type UserService struct { Notifier Notifier}
func (s *UserService) NotifyUser(userID string) error { // Logik zum Abrufen der Benutzer-E-Mail email := userID + "@example.com" return s.Notifier.Send(email, "Welcome!")}1.2. Erstellen eines Mocks mit testify/mock
Installieren Sie die Bibliothek: go get github.com/stretchr/testify.
// notifier_mock.go (im Paket user_test)type NotifierMock struct { mock.Mock}
func (m *NotifierMock) Send(to string, message string) error { args := m.Called(to, message) return args.Error(0)}1.3. Table-Driven Tests
Dies ist ein Standard-Pattern in Go zum Testen vieler Fälle. Es verbessert die Lesbarkeit und vereinfacht das Hinzufügen neuer Tests.
// user_service_test.gofunc TestNotifyUser(t *testing.T) { tests := []struct { name string userID string mockError error expectErr bool }{ {"success", "user1", nil, false}, {"fail_send", "user2", errors.New("send error"), true}, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockNotifier := new(NotifierMock) service := &UserService{Notifier: mockNotifier}
mockNotifier.On("Send", tt.userID+"@example.com", "Welcome!").Return(tt.mockError)
err := service.NotifyUser(tt.userID) if tt.expectErr { assert.Error(t, err) } else { assert.NoError(t, err) } mockNotifier.AssertExpectations(t) }) }}Best Practice: Verwenden Sie t.Run für Subtests – dies ermöglicht es, jeden Fall einzeln auszuführen und zu sehen, welcher genau fehlgeschlagen ist.
2. Integrationstests: Arbeiten mit DB und externen Diensten
Integrationstests überprüfen das Zusammenspiel mehrerer Komponenten: Go-Code mit der Datenbank, API-Server mit dem Cache usw. Die größte Herausforderung ist die Verwaltung des Umgebungszustands. Hier kommen Testcontainers zur Hilfe.
2.1. Testcontainers für Go
Testcontainers ermöglichen es, Docker-Container direkt aus den Tests heraus zu starten. Dies bietet eine saubere, isolierte Umgebung für jeden Durchlauf.
Installation: go get github.com/testcontainers/testcontainers-go