Powrót do strony głównej

Fixtury w Go do testów automatycznych: praktyka z Axiom

Artykuł wyjaśnia, jak używać fixtur w Go do wydzielania logiki infrastrukturalnej z testów automatycznych. Omówiona jest biblioteka Axiom, jej architektura i praktyczne przykłady użycia w scenariuszach integracyjnych.

Fixtury Go: jak pisać czyste testy integracyjne
Advertisement 728x90

Fikstury w Go: wydzielanie infrastruktury z testów automatycznych z Axiom

Integracyjne i E2E-testy w Go często cierpią na mieszanie logiki biznesowej z zarządzaniem infrastrukturą. Standardowy pakiet testing nie oferuje mechanizmu fikstur, dlatego programiści muszą dublować kod inicjalizacji klientów, przygotowania danych i czyszczenia zasobów bezpośrednio wewnątrz testów. To obniża czytelność, komplikuje utrzymanie i utrudnia skalowanie zestawu testów. Rozwiązaniem jest wprowadzenie fikstur za pomocą bibliotek zewnętrznych, takich jak Axiom, które pozwalają deklaratywnie opisywać zależności z zarządzanym cyklem życia.

Co to jest fikstura w kontekście testowania

Fikstura — to nie po prostu funkcja pomocnicza, lecz zasób z wyraźnie określonym cyklem życia: tworzenie na żądanie, automatyczne czyszczenie po wykonaniu testu, możliwość zależności od innych fikstur i izolacja od logiki weryfikacji. W przeciwieństwie do ręcznego zarządzania połączeniami lub danymi, fikstury zapewniają:

  • Leniwą inicjalizację — zasób jest tworzony tylko przy pierwszym użyciu.
  • Automatyczne czyszczenie — nawet w przypadku niepowodzenia testu stan jest gwarantowanie czyszczony.
  • Deklaratywne zależności — jedna fikstura może odnosić się do innej bez ręcznego przekazywania parametrów.
  • Izolację między testami i ponownymi uruchomieniami (retry) — stan nie przecieka między próbami.

W językach takich jak Python (pytest) czy Java (JUnit) takie mechanizmy są wbudowane. W Go trzeba je implementować jawnie lub używać specjalistycznych narzędzi.

Google AdInline article slot

Problem bez fikstur: infrastruktura wewnątrz testu

Typowy test w Go bez fikstur wygląda tak:

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")
    }
}

Tutaj wszystko — od tworzenia klienta po usuwanie użytkownika — znajduje się wewnątrz ciała testu. Przy wzroście liczby zależności (bazy danych, zewnętrzne API, kolejki wiadomości) taki podkhod prowadzi do:

  • Wysokiego poziomu duplikacji kodu.
  • Trudności w wprowadzaniu zmian (np. aktualizacja sposobu autentykacji klienta).
  • Zwiększonego ryzyka błędów w logice czyszczenia.
  • Słabej czytelności: istota sprawdzenia gubi się wśród szczegółów infrastrukturalnych.

Implementacja fikstur za pomocą Axiom

Biblioteka Axiom oferuje mechanizm do opisywania fikstur jako funkcji zwracających wartość, funkcję czyszczenia i błąd. Cyklem życia zarządza Runner i Case.

Google AdInline article slot

Przykład struktury:

// 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")
        }
    })
}

Kluczowe zalety takiego podejścia:

  • Wyraźne rozdzielenie odpowiedzialności: fikstury zajmują się infrastrukturą, test — zachowaniem.
  • Ponowne wykorzystanie: jedna fikstura klienta może być używana we wszystkich testach poprzez Runner.
  • Bezpieczeństwo przy retry: przy ponownym uruchomieniu testu wszystkie fikstury są odtwarzane, eliminując wpływ poprzednich stanów.
  • Deklaratywność: zależności są ogłaszane jawnie, bez imperatywnego kodu w ciele testu.

Kiedy warto wprowadzać fikstury

Fikstury są szczególnie przydatne w następujących scenariuszach:

Google AdInline article slot
  • Testy integracyjne z wieloma zewnętrznymi zależnościami (bazy danych, API, przechowywanie plików).
  • Testy E2E wymagające przygotowania złożonego środowiska.
  • Projekty z dużą liczbą testów (>50–100), gdzie utrzymanie staje się kluczowe.
  • Zespoły dążące do jednolitości i przewidywalności w infrastrukturze testowej.

Nie warto stosować fikstur do prostych testów jednostkowych, gdzie zależności łatwo się mockuje za pomocą interfejsów.

Co ważne

  • Fikstury w Go nie są wbudowane — trzeba je implementować jawnie lub używać bibliotek jak Axiom.
  • Celem fikstur jest wydzielenie logiki infrastrukturalnej poza ciało testu.
  • Axiom zapewnia leniwą inicjalizację, automatyczne czyszczenie i izolację między testami.
  • Podejście jest szczególnie efektywne w dużych projektach z setkami testów integracyjnych.
  • Fikstury nie zastępują mocków — uzupełniają je w scenariuszach wymagających rzeczywistego środowiska.

— Editorial Team

Advertisement 728x90

Czytaj dalej