Testowanie jednostkowe w Go: struktura i najlepsze praktyki
Testowanie w Go od podstaw po zaawansowane wzorce
wbudowanego pakietu testowego w Go daje potężny, minimalistyczny framework do pisania testów jednostkowych bez zależności zewnętrznych. Oto podstawy testowania, struktura projektu i zaawansowane wzorce do tworzenia niezawodnych aplikacji w Go.

Dlaczego testowanie ma znaczenie w Go
Filozofia Go podkreśla prostotę i niezawodność. Biblioteka standardowa zawiera pakiet testing, który sprawia, że testowanie jednostkowe jest pierwszym klasą obywatelstwa w ekosystemie Go. Dobrze przetestowany kod w Go poprawia utrzymanie, wykrywa błędy wczesnie i dostarcza dokumentacji poprzez przykłady. Jeśli jesteś nowy w Go, sprawdź nasz arkusz wskazówek dla Go jako szybki przewodnik po podstawach języka.
Główne korzyści testowania w Go:
- Wbudowana obsługa: Nie wymagane ramy zewnętrzne
- Szybka realizacja: Domyślnie równoległe wykonanie testów
- Prosty składnia: Minimalny kod szkieletowy
- Bogate narzędzia: Raporty pokrycia, testy wydajnościowe i profilowanie
- Przyjazne dla CI/CD: Łatwe integracja z automatycznymi potokami
Struktura projektu dla testów w Go
Testy w Go są umieszczone obok kodu produkcyjnego z wyraźną konwencją nazewnictwa:
myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│ ├── helper.go
│ └── helper_test.go
└── models/
├── user.go
└── user_test.go
Główne konwencje:
- Pliki testowe kończą się na
_test.go - Testy są w tej samej paczce co kod (lub używają sufiksu
_testdla testów czarnych skrzyń) - Każdy plik źródłowy może mieć odpowiadający mu plik testowy
Metody testowania paczki
Testowanie białej skrzynki (ta sama paczka):
package calculator
import "testing"
// Można uzyskać dostęp do nieeksportowanych funkcji i zmiennych
Testowanie czarnej skrzynki (zewnętrzna paczka):
package calculator_test
import (
"testing"
"myproject/calculator"
)
// Można uzyskać dostęp tylko do eksportowanych funkcji (rekomendowane dla publicznych API)
Podstawowa struktura testu
Każda funkcja testowa遵循 ten wzorzec:
package calculator
import "testing"
// Funkcja testowa musi zaczynać się od "Test"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Metody testing.T:
t.Error()/t.Errorf(): Oznacza test jako nieprawidłowy, ale kontynuujet.Fatal()/t.Fatalf(): Oznacza test jako nieprawidłowy i natychmiast zatrzymujet.Log()/t.Logf(): Logowanie wyjścia (wyświetlane tylko z flagą-v)t.Skip()/t.Skipf(): Pomija testt.Parallel(): Uruchamia test równolegle z innymi testami równoległymi
Testy oparte na tabelach: Go styl
Testy oparte na tabelach to idiomiczny sposób Go na testowanie wielu scenariuszy. Z ogólnymi typami w Go, możesz również tworzyć bezpieczne dla typów pomocniki testowe, które działają na różnych typach danych:
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"dodawanie", 2, 3, "+", 5, false},
{"odejmowanie", 5, 3, "-", 2, false},
{"mnożenie", 4, 3, "*", 12, false},
{"dzielenie", 10, 2, "/", 5, false},
{"dzielenie przez zero", 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)
}
})
}
}
Zalety:
- Jedna funkcja testowa dla wielu scenariuszy
- Łatwe dodawanie nowych przypadków testowych
- Jasna dokumentacja oczekiwanej zachowania
- Lepsza organizacja i utrzymanie testów
Uruchamianie testów
Podstawowe polecenia
# Uruchom testy w bieżącym katalogu
go test
# Uruchom testy z wyprowadzaniem szczegółów
go test -v
# Uruchom testy we wszystkich podkatalogach
go test ./...
# Uruchom konkretny test
go test -run TestAdd
# Uruchom testy pasujące do wzorca
go test -run TestCalculate/addition
# Uruchom testy równolegle (domyślnie GOMAXPROCS)
go test -parallel 4
# Uruchom testy z timeoutem
go test -timeout 30s
Pokrycie testowe
# Uruchom testy z pokryciem
go test -cover
# Wygeneruj profil pokrycia
go test -coverprofile=coverage.out
# Wyświetl pokrycie w przeglądarce
go tool cover -html=coverage.out
# Wyświetl pokrycie według funkcji
go tool cover -func=coverage.out
# Ustaw tryb pokrycia (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out
Przydatne flagi
-short: Uruchom testy oznaczoneif testing.Short()-race: Włącz detektor wyścigów (wykrywa problemy z dostępem współbieżnym)-cpu: Określ wartości GOMAXPROCS-count n: Uruchom każdy test n razy-failfast: Zatrzymaj po pierwszym niepowodzeniu testu
Pomocniki testowe i konfiguracja
Funkcje pomocnicze
Oznacz funkcje pomocnicze t.Helper() w celu poprawienia raportowania błędów:
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Ta linia jest raportowana jako wywołująca
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMath(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5) // Linia błędu wskazuje tutaj
}
Konfiguracja i demontaż
func TestMain(m *testing.M) {
// Kod konfiguracji
setup()
// Uruchom testy
code := m.Run()
// Kod demontażu
teardown()
os.Exit(code)
}
Fixtury testowe
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("konfiguracja testu")
return func(t *testing.T) {
t.Log("demontaż testu")
}
}
func TestSomething(t *testing.T) {
teardown := setupTestCase(t)
defer teardown(t)
// Kod testowy
}
Mockowanie i iniekcja zależności
Mockowanie oparte na interfejsach
Gdy testujesz kod, który interaguje z bazami danych, użycie interfejsów ułatwia tworzenie implementacji mock. Jeśli pracujesz z PostgreSQL w Go, zobacz nasz porównanie bibliotek ORM w Go do wyboru odpowiedniej biblioteki bazy danych z dobrym testowalnością.
// Kod produkcyjny
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
}
// Kod testowy
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("nieoczekiwany błąd: %v", err)
}
if name != "Alice" {
t.Errorf("dostałem %s, oczekiwałem Alice", name)
}
}
Popularne biblioteki testowe
Testify
Najpopularniejsza biblioteka testowa w Go do asercji i mockowania:
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, "powinny być równe")
assert.NotNil(t, result)
}
// Przykład mockowania
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)
}
Inne narzędzia
- gomock: Ramy mockowania Google z generowaniem kodu
- httptest: Biblioteka standardowa do testowania handlerów HTTP
- testcontainers-go: Testowanie integracji z kontenerami Docker
- ginkgo/gomega: Ramy testowe stylu BDD
Gdy testujesz integracje z zewnętrznymi usługami, takimi jak modele AI, musisz mockować lub stubować te zależności. Na przykład, jeśli korzystasz z Ollama w Go, rozważ tworzenie wrapperów interfejsów, aby uczynić swój kod bardziej testowalnym.
Testy wydajnościowe
Go zawiera wbudowaną obsługę testów wydajnościowych:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Uruchom testy wydajnościowe
// go test -bench=. -benchmem
Wynik pokazuje liczby iteracji na sekundę i alokacje pamięci.
Najlepsze praktyki
- Pisz testy oparte na tabelach: Używaj wzorca struktury wycinku do wielu przypadków testowych
- Używaj
t.Rundla podtestów: Lepsza organizacja i możliwość wyboru wykonywania podtestów - Testuj najpierw funkcje eksportowane: Skup się na zachowaniu publicznego API
- Zachowuj testy proste: Każdy test powinien weryfikować jedną rzecz
- Używaj znaczących nazw testów: Opisuj, co jest testowane i oczekiwany wynik
- Nie testuj szczegółów implementacji: Testuj zachowanie, nie wewnętrzne
- Używaj interfejsów dla zależności: Ułatwia mockowanie
- Dąż do wysokiego pokrycia, ale jakość nad ilością: 100% pokrycia nie oznacza braku błędów
- Uruchamiaj testy z flagą -race: Wykrywaj problemy współbieżne wczesnie
- Używaj
TestMaindla kosztownej konfiguracji: Unikaj powtarzania konfiguracji w każdym teście
Przykład: Pełny zestaw testów
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("imię nie może być puste")
}
if u.Email == "" {
return errors.New("adres e-mail nie może być pusty")
}
return nil
}
// Plik testowy: user_test.go
func TestValidateUser(t *testing.T) {
tests := []struct {
name string
user *User
wantErr bool
errMsg string
}{
{
name: "prawidłowy użytkownik",
user: &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
wantErr: false,
},
{
name: "puste imię",
user: &User{ID: 1, Name: "", Email: "alice@example.com"},
wantErr: true,
errMsg: "imię nie może być puste",
},
{
name: "pusty adres e-mail",
user: &User{ID: 1, Name: "Alice", Email: ""},
wantErr: true,
errMsg: "adres e-mail nie może być pusty",
},
}
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)
}
})
}
}
Przydatne linki
- Oficjalna dokumentacja pakietu testowego Go
- Blog Go: Testy oparte na tabelach
- Repozytorium GitHub Testify
- Dokumentacja GoMock
- Naucz się Go z testami
- Narzędzie do pokrycia kodu Go
- Arkusz wskazówek dla Go
- Porównanie bibliotek ORM w Go dla PostgreSQL: GORM vs Ent vs Bun vs sqlc
- SDK Go dla Ollama - porównanie z przykładami
- Tworzenie aplikacji CLI w Go z Cobra & Viper
- Ogólne typy w Go: przypadki użycia i wzorce
Podsumowanie
Ramy testowe Go dostarczają wszystkiego, co potrzebne do kompleksowego testowania jednostkowego z minimalnym ustawieniem. Przyjmując idiomy Go, takie jak testy oparte na tabelach, używając interfejsów do mockowania i korzystając z wbudowanych narzędzi, możesz tworzyć utrzyjmalne, niezawodne zestawy testów, które rosną wraz z Twoją bazą kodu.
Te praktyki testowania stosują się do wszystkich typów aplikacji w Go, od usług sieciowych po aplikacje CLI zbudowane z Cobra & Viper. Testowanie narzędzi CLI wymaga podobnych wzorców z dodatkowym akcentem na testowanie wejścia/wyjścia i analizy flag.
Zacznij od prostych testów, stopniowo dodawaj pokrycie i pamiętaj, że testowanie to inwestycja w jakość kodu i zaufanie do programisty. Akcent społeczności Go na testowanie ułatwia utrzymanie projektów w dłuższej perspektywie i skuteczne współpracę z członkami zespołu.