Go Unit Testing: Struktur & Best Practices
Testen von Grundlagen bis zu fortgeschrittenen Mustern
Go’s eingebaute Testpaket bietet einen leistungsstarken, minimalistischen Rahmen für das Schreiben von Einheitstests ohne externe Abhängigkeiten. Hier sind die Testgrundlagen, die Projektstruktur und fortgeschrittene Muster, um zuverlässige Go-Anwendungen zu erstellen.

Warum Testing in Go wichtig ist
Die Philosophie von Go betont Einfachheit und Zuverlässigkeit. Die Standardbibliothek enthält das testing-Paket, wodurch Einheitstests zu einer erstklassigen Funktion im Go-Ökosystem werden. Gut getesteter Go-Code verbessert die Wartbarkeit, erkennt Fehler frühzeitig und bietet Dokumentation durch Beispiele. Wenn Sie neu in Go sind, werfen Sie einen Blick auf unseren Go Cheat Sheet für einen schnellen Überblick über die Grundlagen der Sprache.
Wichtige Vorteile von Go-Tests:
- Eingebaute Unterstützung: Keine externen Frameworks erforderlich
- Schnelle Ausführung: Standardmäßig parallele Testausführung
- Einfache Syntax: Minimale Boilerplate-Code
- Umfassende Tools: Abdeckungsberichte, Benchmarks und Profiling
- CI/CD-freundlich: Einfache Integration in automatisierte Pipelines
Projektstruktur für Go-Tests
Go-Tests befinden sich neben dem Produktionscode mit einer klaren Namenskonvention:
myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│ ├── helper.go
│ └── helper_test.go
└── models/
├── user.go
└── user_test.go
Wichtige Konventionen:
- Testdateien enden mit
_test.go - Tests befinden sich im gleichen Paket wie der Code (oder verwenden den
_test-Suffix für Black-Box-Tests) - Jede Quelldatei kann eine entsprechende Testdatei haben
Paket-Testansätze
White-Box-Testing (gleiches Paket):
package calculator
import "testing"
// Kann nicht exportierte Funktionen und Variablen zugreifen
Black-Box-Testing (externes Paket):
package calculator_test
import (
"testing"
"myproject/calculator"
)
// Kann nur exportierte Funktionen zugreifen (empfohlen für öffentliche APIs)
Grundlegende Teststruktur
Jede Testfunktion folgt diesem Muster:
package calculator
import "testing"
// Testfunktion muss mit "Test" beginnen
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Testing.T-Methoden:
t.Error()/t.Errorf(): Markiere Test als fehlgeschlagen, aber setze fortt.Fatal()/t.Fatalf(): Markiere Test als fehlgeschlagen und stoppe sofortt.Log()/t.Logf(): Protokollausgabe (nur mit-v-Flag angezeigt)t.Skip()/t.Skipf(): Überspringe den Testt.Parallel(): Führe Test parallel mit anderen parallelen Tests aus
Tabellengetriebene Tests: Der Go-Weg
Tabellengetriebene Tests sind der idiomatische Go-Ansatz zum Testen mehrerer Szenarien. Mit Go-Generics können Sie auch typsichere Test-Helper erstellen, die mit verschiedenen Datentypen funktionieren:
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"Addition", 2, 3, "+", 5, false},
{"Subtraktion", 5, 3, "-", 2, false},
{"Multiplikation", 4, 3, "*", 12, false},
{"Division", 10, 2, "/", 5, false},
{"Division durch Null", 10, 0, "/", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Calculate(tt.a, tt.b, tt.op)
if (err != nil) != tt.wantErr {
t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("Calculate(%d, %d, %q) = %d; want %d",
tt.a, tt.b, tt.op, result, tt.expected)
}
})
}
}
Vorteile:
- Eine Testfunktion für mehrere Szenarien
- Einfache Hinzufügung neuer Testfälle
- Klare Dokumentation des erwarteten Verhaltens
- Bessere Testorganisation und -wartbarkeit
Tests ausführen
Grundlegende Befehle
# Führe Tests im aktuellen Verzeichnis aus
go test
# Führe Tests mit detaillierter Ausgabe aus
go test -v
# Führe Tests in allen Unterverzeichnissen aus
go test ./...
# Führe einen bestimmten Test aus
go test -run TestAdd
# Führe Tests aus, die einem Muster entsprechen
go test -run TestCalculate/addition
# Führe Tests parallel aus (Standard ist GOMAXPROCS)
go test -parallel 4
# Führe Tests mit Timeout aus
go test -timeout 30s
Testabdeckung
# Führe Tests mit Abdeckung aus
go test -cover
# Generiere Abdeckungsprofil
go test -coverprofile=coverage.out
# Zeige Abdeckung im Browser
go tool cover -html=coverage.out
# Zeige Abdeckung nach Funktion
go tool cover -func=coverage.out
# Setze Abdeckungsmodus (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out
Nützliche Flags
-short: Führe Tests aus, die mitif testing.Short()-Prüfungen markiert sind-race: Aktiviere Race-Detector (findet Probleme mit gleichzeitigem Zugriff)-cpu: Geben Sie GOMAXPROCS-Werte an-count n: Führe jeden Test n Mal aus-failfast: Stoppen Sie beim ersten Testfehler
Test-Helper und Setup/Teardown
Helper-Funktionen
Markieren Sie Helper-Funktionen mit t.Helper(), um die Fehlerberichterstattung zu verbessern:
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Diese Zeile wird als Aufrufer gemeldet
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMath(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5) // Fehlerzeile zeigt hier
}
Setup und Teardown
func TestMain(m *testing.M) {
// Setup-Code hier
setup()
// Führe Tests aus
code := m.Run()
// Teardown-Code hier
teardown()
os.Exit(code)
}
Test-Fixtures
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("setup test case")
return func(t *testing.T) {
t.Log("teardown test case")
}
}
func TestSomething(t *testing.T) {
teardown := setupTestCase(t)
defer teardown(t)
// Test-Code hier
}
Mocking und Dependency Injection
Interface-basiertes Mocking
Wenn Sie Code testen, der mit Datenbanken interagiert, macht es das Erstellen von Mock-Implementierungen einfach, wenn Sie Schnittstellen verwenden. Wenn Sie mit PostgreSQL in Go arbeiten, werfen Sie einen Blick auf unseren Vergleich von Go-ORMs zur Auswahl der richtigen Datenbankbibliothek mit guter Testbarkeit.
// Produktionscode
type Database interface {
GetUser(id int) (*User, error)
}
type UserService struct {
db Database
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.db.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
// Testcode
type MockDatabase struct {
users map[int]*User
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, errors.New("user not found")
}
func TestGetUserName(t *testing.T) {
mockDB := &MockDatabase{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
},
}
service := &UserService{db: mockDB}
name, err := service.GetUserName(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if name != "Alice" {
t.Errorf("got %s, want Alice", name)
}
}
Beliebte Testbibliotheken
Testify
Die beliebteste Go-Testbibliothek für Assertions und Mocks:
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWithTestify(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result, "they should be equal")
assert.NotNil(t, result)
}
// Mock-Beispiel
type MockDB struct {
mock.Mock
}
func (m *MockDB) GetUser(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
Andere Tools
- gomock: Googles Mocking-Framework mit Codegenerierung
- httptest: Standardbibliothek zum Testen von HTTP-Handlern
- testcontainers-go: Integrationstests mit Docker-Containern
- ginkgo/gomega: BDD-ähnlicher Testframework
Wenn Sie Integrationen mit externen Diensten wie KI-Modellen testen, müssen Sie diese Abhängigkeiten mocken oder stubben. Wenn Sie beispielsweise Ollama in Go verwenden, sollten Sie Schnittstellenumhüllungen erstellen, um Ihren Code testbarer zu machen.
Benchmark-Tests
Go bietet eingebaute Unterstützung für Benchmarks:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Führe Benchmarks aus
// go test -bench=. -benchmem
Die Ausgabe zeigt Iterationen pro Sekunde und Speicherzuweisungen.
Best Practices
- Schreiben Sie tabellengetriebene Tests: Verwenden Sie das Slice-of-Structs-Muster für mehrere Testfälle
- Verwenden Sie t.Run für Untertests: Bessere Organisation und selektive Ausführung von Untertests
- Testen Sie exportierte Funktionen zuerst: Konzentrieren Sie sich auf das Verhalten der öffentlichen API
- Halten Sie Tests einfach: Jeder Test sollte eine Sache überprüfen
- Verwenden Sie aussagekräftige Testnamen: Beschreiben Sie, was getestet wird und welches Ergebnis erwartet wird
- Testen Sie keine Implementierungsdetails: Testen Sie Verhalten, nicht Interna
- Verwenden Sie Schnittstellen für Abhängigkeiten: Erleichtert das Mocking
- Streben Sie hohe Abdeckung an, aber Qualität vor Quantität: 100% Abdeckung bedeutet nicht fehlerfrei
- Führen Sie Tests mit dem -race-Flag aus: Erkennen Sie Concurrency-Probleme frühzeitig
- Verwenden Sie TestMain für teuren Setup: Vermeiden Sie wiederholten Setup in jedem Test
Beispiel: Vollständiger Testsuite
package user
import (
"errors"
"testing"
)
type User struct {
ID int
Name string
Email string
}
func ValidateUser(u *User) error {
if u.Name == "" {
return errors.New("name cannot be empty")
}
if u.Email == "" {
return errors.New("email cannot be empty")
}
return nil
}
// Testdatei: user_test.go
func TestValidateUser(t *testing.T) {
tests := []struct {
name string
user *User
wantErr bool
errMsg string
}{
{
name: "gültiger Benutzer",
user: &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
wantErr: false,
},
{
name: "leerer Name",
user: &User{ID: 1, Name: "", Email: "alice@example.com"},
wantErr: true,
errMsg: "name cannot be empty",
},
{
name: "leere E-Mail",
user: &User{ID: 1, Name: "Alice", Email: ""},
wantErr: true,
errMsg: "email cannot be empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.user)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}
Nützliche Links
- Offizielle Go Testing Package Dokumentation
- Go Blog: Table-Driven Tests
- Testify GitHub Repository
- GoMock Dokumentation
- Learn Go with Tests
- Go Code Coverage Tool
- Go Cheat Sheet
- Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Go SDKs für Ollama - Vergleich mit Beispielen
- Erstellung von CLI-Anwendungen in Go mit Cobra & Viper
- Go Generics: Anwendungsfälle und Muster
Fazit
Das Testframework von Go bietet alles, was für umfassende Unit-Tests mit minimaler Einrichtung benötigt wird. Durch die Verwendung von Go-Idiomen wie tabellengetriebenen Tests, die Nutzung von Schnittstellen zum Mocking und die Ausnutzung eingebauter Tools können Sie wartbare, zuverlässige Testsuiten erstellen, die mit Ihrer Codebasis wachsen.
Diese Testpraktiken gelten für alle Arten von Go-Anwendungen, von Webdiensten bis hin zu CLI-Anwendungen, die mit Cobra & Viper erstellt wurden. Das Testen von Command-Line-Tools erfordert ähnliche Muster mit zusätzlichem Fokus auf das Testen von Eingabe/Ausgabe und Flag-Analyse.
Beginnen Sie mit einfachen Tests, erweitern Sie schrittweise die Abdeckung und denken Sie daran, dass Tests eine Investition in die Codequalität und das Vertrauen der Entwickler sind. Der Fokus der Go-Community auf Tests macht es einfacher, Projekte langfristig zu warten und effektiv mit Teammitgliedern zusammenzuarbeiten.