Fixtures in Go: Extrahieren der Infrastruktur aus automatisierten Tests mit Axiom
Integrations- und E2E-Tests in Go leiden oft darunter, dass Geschäftslogik mit der Verwaltung von Infrastruktur vermischt wird. Das Standardpaket testing bietet keinen Mechanismus für Fixtures, was Entwickler zwingt, Code für die Initialisierung von Clients, Vorbereitung von Daten und das Aufräumen von Ressourcen direkt in den Tests zu duplizieren. Das mindert die Lesbarkeit, erschwert die Wartung und behindert die Skalierung der Test-Suite. Die Lösung ist die Implementierung von Fixtures über Drittanbieter-Bibliotheken wie Axiom, die es ermöglichen, Abhängigkeiten deklarativ mit verwalteten Lebenszyklen zu beschreiben.
Was sind Fixtures im Kontext des Testens
Ein Fixture ist nicht nur eine Hilfsfunktion – es handelt sich um eine Ressource mit einem klar definierten Lebenszyklus: Erstellung auf Abruf, automatisches Aufräumen nach der Testausführung, Möglichkeit der Abhängigkeit von anderen Fixtures und Isolation von der Assertions-Logik. Im Gegensatz zur manuellen Verwaltung von Verbindungen oder Daten bieten Fixtures:
- Lazy-Initialisierung – die Ressource wird nur beim ersten Zugriff erstellt.
- Automatisches Aufräumen – selbst wenn der Test fehlschlägt, wird der Zustand garantiert bereinigt.
- Deklarative Abhängigkeiten – ein Fixture kann ein anderes referenzieren, ohne manuelle Parameterübergabe.
- Isolation zwischen Tests und Wiederholungen – Zustände leaken nicht zwischen Versuchen.
Sprachen wie Python (pytest) oder Java (JUnit) haben diese Mechanismen integriert. In Go müssen sie explizit implementiert oder mit spezialisierten Tools umgesetzt werden.
Das Problem ohne Fixtures: Infrastruktur direkt im Test
Ein typischer Go-Test ohne Fixtures sieht so aus:
func TestGetUser(t *testing.T) {
client, err := NewUserClient()
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
defer client.Close()
user, err := CreateUser(client)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
defer func() {
_ = DeleteUser(client, user.ID)
}()
resp, err := client.GetUser(user.ID)
if err != nil {
t.Fatalf("failed to get user: %v", err)
}
if resp.ID != user.ID {
t.Fatalf("unexpected user id")
}
}
Hier ist alles – vom Erstellen des Clients bis zum Löschen des Benutzers – direkt im Testkörper. Wenn die Anzahl der Abhängigkeiten wächst (Datenbanken, externe APIs, Message Queues), führt dieser Ansatz zu:
- Hoher Code-Duplikation.
- Schwierigkeit bei Änderungen (z. B. Aktualisierung der Client-Authentifizierung).
- Erhöhtem Risiko von Fehlern in der Aufräumlogik.
- Schlechter Lesbarkeit: Die eigentliche Assertion geht in den Infrastrukturdetails unter.
Fixtures mit Axiom implementieren
Die Axiom-Bibliothek bietet einen Mechanismus, um Fixtures als Funktionen zu beschreiben, die einen Wert, eine Aufräumfunktion und einen Fehler zurückgeben. Der Lebenszyklus wird über Runner und Case verwaltet.
Beispielstruktur:
// Fixture client
func UserClientFixture(_ *axiom.Config) (any, func(), error) {
client, err := NewUserClient()
if err != nil {
return nil, nil, err
}
return client, client.Close, nil
}
// Fixture polzovatelya, zavisyaschaya from client
func UserFixture(cfg *axiom.Config) (any, func(), error) {
client := axiom.GetFixture*UserClient
user, err := client.CreateUser()
if err != nil {
return nil, nil, err
}
cleanup := func() { _ = client.DeleteUser(user.ID) }
return user, cleanup, nil
}
// Runner with globalnoy fiksturoy
var runner = axiom.NewRunner(
axiom.WithRunnerFixture("client", UserClientFixture),
)
// Test
func TestGetUser(t *testing.T) {
c := axiom.NewCase(
axiom.WithCaseName("get user by id"),
axiom.WithCaseFixture("user", UserFixture),
)
runner.RunCase(t, c, func(cfg *axiom.Config) {
client := axiom.GetFixture*UserClient
user := axiom.GetFixture*User
resp, err := client.GetUser(user.ID)
if err != nil {
t.Fatalf("failed to get user: %v", err)
}
if resp.ID != user.ID {
t.Fatalf("unexpected user id")
}
})
}
Schlüsselvorteile dieses Ansatzes:
- Klare Trennung der Verantwortlichkeiten: Fixtures kümmern sich um die Infrastruktur, Tests um das Verhalten.
- Wiederverwendbarkeit: Ein einzelnes Client-Fixture kann über Runner in allen Tests verwendet werden.
- Sicherheit bei Wiederholungen: Bei Testwiederholungen werden alle Fixtures neu erstellt, um Einfluss vorheriger Zustände zu vermeiden.
- Deklarativ: Abhängigkeiten werden explizit deklariert, ohne imperativen Code im Testkörper.
Wann Fixtures implementieren
Fixtures sind besonders nützlich in diesen Szenarien:
- Integrationstests mit mehreren externen Abhängigkeiten (DB, API, Dateispeicher).
- E2E-Tests, die eine komplexe Umgebungseinrichtung erfordern.
- Projekte mit einer großen Anzahl von Tests (>50–100), bei denen die Wartung entscheidend ist.
- Teams, die Uniformität und Vorhersagbarkeit in der Testinfrastruktur anstreben.
Fixtures nicht für einfache Unittests verwenden, bei denen Abhängigkeiten leicht über Interfaces gemockt werden können.
Wichtige Punkte
- Fixtures sind nicht in Go integriert – Sie müssen sie explizit implementieren oder Bibliotheken wie Axiom verwenden.
- Das Ziel von Fixtures ist es, die Infrastruktur-Logik aus dem Testkörper herauszuziehen.
- Axiom bietet Lazy-Initialisierung, automatisches Aufräumen und Isolation zwischen Tests.
- Der Ansatz ist besonders effektiv in großen Projekten mit Hunderten von Integrationstests.
- Fixtures ersetzen keine Mocks – sie ergänzen sie in Szenarien mit realen Umgebungen.
— Editorial Team
Noch keine Kommentare.