Testowanie w Go w 2026: Testy jednostkowe, mocki i pytania rekrutacyjne

Opanuj testowanie w Go z biblioteką standardową, testami tabelarycznymi, mockami i wzorcami oczekiwanymi na rozmowach kwalifikacyjnych. Praktyczne przykłady z testify, gomock i pakietem testing.

Testowanie Go, testy jednostkowe i mocki na rozmowach rekrutacyjnych

Testowanie w Go opiera się na standardowym pakiecie testing, który jest dostarczany z każdą instalacją Go i nie wymaga żadnych zewnętrznych zależności. W przeciwieństwie do frameworków w innych językach, podejście Go preferuje prostotę: pliki testowe znajdują się obok kodu produkcyjnego, funkcje testowe przestrzegają konwencji nazewnictwa, a polecenie go test obsługuje wykrywanie i wykonywanie testów.

Czego szukają rekruterzy

Kandydatów, którzy piszą testy tabelaryczne, używają interfejsów do wstrzykiwania zależności i rozumieją, kiedy mockowanie wnosi wartość, a kiedy tylko niepotrzebną złożoność.

Pisanie testów jednostkowych z pakietem testing

Każdy plik testowy kończy się na _test.go i znajduje się w tym samym pakiecie co testowany kod. Funkcje testowe zaczynają się od Test i nazwy pisanej wielką literą. Parametr *testing.T dostarcza metod do raportowania błędów.

calculator_test.gogo
package calculator

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

Uruchomienie go test ./... wykonuje wszystkie testy w bieżącym module. Flaga -v pokazuje nazwy poszczególnych testów i ich status pass/fail.

Testy tabelaryczne: standard w Go

Testy tabelaryczne redukują duplikację i czynią dodawanie nowych przypadków trywialnym. Każdy wiersz w tabeli reprezentuje jeden scenariusz z danymi wejściowymi i oczekiwanymi wynikami. Ten wzorzec występuje w bibliotece standardowej Go oraz w produkcyjnych bazach kodu w całej branży.

validator_test.gogo
package validator

import "testing"

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {"valid email", "user@example.com", false},
        {"missing at sign", "userexample.com", true},
        {"empty string", "", true},
        {"unicode local part", "user@例え.jp", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.email)
            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
                    tt.email, err, tt.wantErr)
            }
        })
    }
}

Metoda t.Run tworzy podtesty, które działają niezależnie. Nieudane podtesty raportują, który konkretny przypadek zawiódł, nie zatrzymując pozostałych przypadków.

Wstrzykiwanie zależności przez interfejsy

Interfejsy Go umożliwiają testowanie kodu zależnego od systemów zewnętrznych: baz danych, klientów HTTP, systemów plików. Definiowanie minimalnego interfejsu w miejscu użycia pozwala na zamianę prawdziwych implementacji na testowe dublerów.

service.gogo
package order

// Repository defines the data access methods this service needs
type Repository interface {
    FindByID(id string) (*Order, error)
    Save(order *Order) error
}

// Service handles order business logic
type Service struct {
    repo Repository
}

// NewService creates a service with the given repository
func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

// Process validates and saves an order
func (s *Service) Process(order *Order) error {
    if order.Total <= 0 {
        return ErrInvalidTotal
    }
    return s.repo.Save(order)
}

Service akceptuje dowolny typ spełniający interfejs Repository. Kod produkcyjny przekazuje implementację opartą na bazie danych. Testy przekazują mock lub stub.

Mockowanie z gomock i mockgen

Pakiet gomock generuje implementacje mocków z definicji interfejsów. Polecenie mockgen odczytuje pliki źródłowe i produkuje struktury mocków, które rejestrują wywołania i zwracają skonfigurowane wartości.

bash
# Generate mocks for the Repository interface
mockgen -source=service.go -destination=mocks/repository_mock.go -package=mocks

Użycie wygenerowanego mocka w testach:

service_test.gogo
package order

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"

    "yourmodule/order/mocks"
)

func TestService_Process_SavesValidOrder(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    order := &Order{ID: "123", Total: 99.99}

    // Expect Save to be called once with this order, return nil
    mockRepo.EXPECT().
        Save(order).
        Return(nil).
        Times(1)

    svc := NewService(mockRepo)
    err := svc.Process(order)

    assert.NoError(t, err)
}

func TestService_Process_RejectsZeroTotal(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    // No calls expected to repo since validation fails first

    svc := NewService(mockRepo)
    err := svc.Process(&Order{ID: "456", Total: 0})

    assert.ErrorIs(t, err, ErrInvalidTotal)
}

Mock weryfikuje, że Save została wywołana dokładnie raz z oczekiwanym argumentem. Jeśli testowany kod nigdy nie wywoła Save lub wywoła ją z innymi argumentami, test nie przejdzie.

Używanie testify do asercji i zestawów testów

testify dostarcza funkcje asercji, które produkują czytelniejsze komunikaty o błędach niż ręczne sprawdzenia if. Pakiet assert kontynuuje wykonywanie po błędach. Pakiet require zatrzymuje test natychmiast.

user_test.gogo
package user

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestParseUser(t *testing.T) {
    input := `{"id": "u1", "name": "Alice"}`

    user, err := ParseUser(input)

    require.NoError(t, err, "parsing should not fail")
    assert.Equal(t, "u1", user.ID)
    assert.Equal(t, "Alice", user.Name)
    assert.Empty(t, user.Email, "email should default to empty")
}

Wywołanie require.NoError zatrzymuje test, jeśli parsowanie nie powiedzie się, zapobiegając panikom typu nil pointer w kolejnych asercjach.

Gotowy na rozmowy o Go?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Testowanie handlerów HTTP z httptest

Pakiet net/http/httptest dostarcza ResponseRecorder do przechwytywania wyjścia handlera oraz Server do uruchamiania lokalnego serwera testowego. Większość testów jednostkowych używa ResponseRecorder, aby uniknąć narzutu sieciowego.

handler_test.gogo
package api

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestHealthHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    rec := httptest.NewRecorder()

    HealthHandler(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `{"status": "ok"}`, rec.Body.String())
}

Dla handlerów wywołujących zewnętrzne serwisy należy wstrzyknąć mock klienta lub użyć httptest.NewServer do utworzenia fałszywego backendu.

Typowe pytania rekrutacyjne dotyczące testowania w Go

Rekruterzy oceniają wiedzę o testowaniu poprzez pytania, które ujawniają doświadczenie z prawdziwymi bazami kodu. Oto wzorce wyróżniające przygotowanych kandydatów:

"Jak przetestowałbyś funkcję wywołującą zewnętrzne API?"

Zdefiniuj interfejs dla klienta HTTP, wstrzyknij go do funkcji lub struktury i dostarcz mock zwracający ustalone odpowiedzi. To eliminuje wywołania sieciowe i czyni testy deterministycznymi.

"Kiedy użyłbyś stuba zamiast mocka?"

Study zwracają ustalone odpowiedzi bez weryfikowania sposobu ich wywołania. Mocki weryfikują, że określone metody zostały wywołane z określonymi argumentami. Używaj stubów, gdy wartość zwracana jest ważniejsza niż interakcja. Używaj mocków, gdy weryfikacja interakcji jest celem testu.

"Czym są testy tabelaryczne i dlaczego Go je preferuje?"

Testy tabelaryczne definiują przypadki testowe jako dane w slice struktur. Redukują boilerplate, czynią dodawanie przypadków trywialnym i produkują czytelne wyjście testowe z t.Run. Wzorzec jest zgodny z preferencją Go dla jawnego, powtarzalnego kodu zamiast sprytnych abstrakcji.

"Jak obsługujesz fixtures testowe lub setup współdzielony przez wiele testów?"

Użyj TestMain do setupu i teardownu na poziomie pakietu. Używaj funkcji pomocniczych lub zestawów testów (z testify) do współdzielonego setupu między powiązanymi testami. Unikaj globalnego stanu, który tworzy sprzężenie między testami.

Przygotowujesz się do rozmowy rekrutacyjnej o Go? Moduł testowania Go szczegółowo omawia te wzorce wraz z pytaniami praktycznymi.

Uruchamianie testów z pokryciem i wykrywaniem wyścigów

Polecenie go test akceptuje flagi ujawniające nietestowane ścieżki kodu i błędy współbieżności.

bash
# Run tests with coverage report
go test -cover ./...

# Generate HTML coverage visualization
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# Detect race conditions (slower, but catches real bugs)
go test -race ./...

Detektor wyścigów instrumentuje dostępy do pamięci i oznacza równoczesne odczyty i zapisy do tej samej zmiennej. Uruchamianie testów z -race w CI wyłapuje błędy, które manifestują się tylko przy określonym timingu.

Organizacja testów w dużych bazach kodu

W miarę wzrostu projektów organizacja testów wpływa na utrzymywalność:

  • Testy w tym samym pakiecie (package foo) mają dostęp do nieeksportowanych funkcji i pól. Używaj do testów jednostkowych weryfikujących wewnętrzne zachowanie.
  • Testy w zewnętrznym pakiecie (package foo_test) importują pakiet jak kod zewnętrzny. Używaj do testów integracyjnych i do weryfikacji publicznego API.
  • Katalogi testdata przechowują fixtures takie jak pliki JSON, golden outputs lub seed databases. Go ignoruje je podczas budowania.
  • Build tags oddzielają testy jednostkowe od testów integracyjnych wymagających systemów zewnętrznych. Uruchom go test -tags=integration ./..., aby je uwzględnić.

Więcej o dobrych praktykach Go poza testowaniem znajdziesz w artykule o wzorcach współbieżności, który omawia gorutyny i kanały.

Co zapamiętać o testowaniu w Go

  • Pakiet testing jest dostarczany z Go i nie wymaga zewnętrznych zależności
  • Testy tabelaryczne redukują duplikację i są oczekiwanym wzorcem na rozmowach rekrutacyjnych
  • Interfejsy umożliwiają wstrzykiwanie zależności; definiuj je w miejscu użycia, nie w implementacji
  • gomock generuje mocki z interfejsów; testify dostarcza czytelniejsze asercje
  • httptest.ResponseRecorder przechwytuje wyjście handlera bez narzutu sieciowego
  • Flaga -race wykrywa błędy współbieżności, które testy jednostkowe inaczej pomijają
  • Metryki pokrycia kierują uwagą, ale nie gwarantują poprawności

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w Go?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 24 sierpnia 2026

Tagi

#go
#testing
#unit-tests
#mocks
#interview

Udostępnij

Powiązane artykuły