Integrationstests mit Go und Docker: Von der Theorie zur fortgeschrittenen Praxis
\n\nIntegrationstests sind ein kritisch wichtiger Schritt in der Entwicklung von Microservices mit Go. Ohne die Überprüfung der Interaktion mit Datenbanken, Nachrichtenwarteschlangen und externen APIs riskieren Sie ein "funktioniert auf meinem Rechner" in seiner unangenehmsten Form. Docker ermöglicht es, für jeden Test saubere, isolierte Umgebungen bereitzustellen, und fortgeschrittene Techniken machen dies schnell, parallel und schmerzfrei. In diesem Artikel behandeln wir die realen Ansätze, die Senior-Ingenieure verwenden.
\n\n1. Warum Docker der ideale Partner für Integrationstests in Go ist
\n\nGo wird zu einer statischen Binärdatei kompiliert, was es zu einem idealen Kandidaten für die Containerisierung macht. Beim Integrationstesten löst Docker drei zentrale Aufgaben:
\n- \n
- Isolation: Jeder Test erhält eine frische Datenbank oder Redis ohne "Müll" von vorherigen Läufen. \n
- Reproduzierbarkeit: Tests funktionieren auf dem Entwicklerrechner, in CI/CD und in der Produktion gleich. \n
- Geschwindigkeit: Richtig konfigurierte Container starten in Sekunden, nicht Minuten. \n
Der Hauptfehler von Anfängern ist das Starten eines einzigen Containers für alle Tests. Dies führt zu Race Conditions und "flackernden" Fehlern. Der fortgeschrittene Ansatz: Ein Container pro Testpaket oder sogar pro einzelnen Test.
\n\n2. Testcontainers für Go: Verwaltete Container aus dem Code
\n\nDie Bibliothek testcontainers-go ermöglicht die Verwaltung von Docker-Containern direkt aus Go-Tests. Dies ist eleganter, als Bash-Skripte zu schreiben oder sich auf einen vorab gestarteten Docker Compose zu verlassen.
Installation:
\ngo get github.com/testcontainers/testcontainers-go\ngo get github.com/testcontainers/testcontainers-go/modules/postgres\n\nBeispiel: Testen von PostgreSQL mit Migrationen:
\npackage integration_test\n\nimport (\n "context"\n "database/sql"\n "testing"\n "time"\n\n "github.com/testcontainers/testcontainers-go"\n "github.com/testcontainers/testcontainers-go/modules/postgres"\n "github.com/testcontainers/testcontainers-go/wait"\n _ "github.com/lib/pq"\n)\n\nfunc TestUserRepository(t *testing.T) {\n ctx := context.Background()\n\n pgContainer, err := postgres.RunContainer(ctx,\n testcontainers.WithImage("postgres:16-alpine"),\n postgres.WithDatabase("testdb"),\n postgres.WithUsername("user"),\n postgres.WithPassword("password"),\n testcontainers.WithWaitStrategy(\n wait.ForLog("database system is ready to accept connections").\n WithOccurrence(2).\n WithStartupTimeout(30*time.Second),\n ),\n )\n if err != nil {\n t.Fatal(err)\n }\n defer pgContainer.Terminate(ctx)\n\n connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")\n if err != nil {\n t.Fatal(err)\n }\n\n db, err := sql.Open("postgres", connStr)\n if err != nil {\n t.Fatal(err)\n }\n defer db.Close()\n\n // Hier führen wir Migrationen und Tests durch\n _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL\n )`)\n if err != nil {\n t.Fatal(err)\n }\n\n // Testen des Repository\n repo := NewUserRepository(db)\n user, err := repo.Create(ctx, "Alice")\n if err != nil {\n t.Fatal(err)\n }\n if user.Name != "Alice" {\n t.Errorf("expected Alice, got %s", user.Name)\n }\n}\n\nWichtige Punkte:
\n- \n
WithWaitStrategy— wir warten, bis PostgreSQL tatsächlich bereit ist, nicht nur, bis der Container gestartet ist. \n defer pgContainer.Terminate(ctx)— garantiert die Bereinigung auch bei einer Panik. \n - Jeder Test erhält seine eigene Datenbankinstanz — vollständige Isolation. \n
3. Parallele Tests und Ressourcenverwaltung
\n\nGo unterstützt parallel