Основы тестирования в Go
Go имеет встроенный пакет testing для тестирования. Тесты — это файлы с суффиксом _test.go.
▸Простой тест
1// calculator.go2package main34func Add(a, b int) int {5 return a + b6}78// calculator_test.go9package main1011import "testing"1213func TestAdd(t *testing.T) {14 result := Add(2, 3)15 if result != 5 {16 t.Errorf("Add(2, 3) = %d; want 5", result)17 }18}
▸Запуск тестов
1# Все тесты2go test ./...34# Конкретный пакет5go test ./pkg/calculator67# С verbose выводом8go test -v ./...910# С фильтром11go test -run TestAdd ./...1213# Тесты с build tags14go test -tags=integration ./...
Table-Driven Tests
▸Паттерн table-driven tests
1func TestAdd(t *testing.T) {2 tests := []struct {3 name string4 a, b int5 expected int6 }{7 {"positive numbers", 2, 3, 5},8 {"negative numbers", -1, -1, -2},9 {"zero", 0, 0, 0},10 {"mixed", -5, 5, 0},11 }1213 for _, tt := range tests {14 t.Run(tt.name, func(t *testing.T) {15 result := Add(tt.a, tt.b)16 if result != tt.expected {17 t.Errorf("Add(%d, %d) = %d; want %d",18 tt.a, tt.b, result, tt.expected)19 }20 })21 }22}
▸Table-driven с подтестами
1func TestDivide(t *testing.T) {2 tests := []struct {3 name string4 a, b float645 expected float646 wantError bool7 }{8 {"normal division", 10, 2, 5, false},9 {"by zero", 10, 0, 0, true},10 }1112 for _, tt := range tests {13 t.Run(tt.name, func(t *testing.T) {14 result, err := Divide(tt.a, tt.b)1516 if tt.wantError {17 if err == nil {18 t.Error("expected error, got nil")19 }20 return21 }2223 if err != nil {24 t.Fatalf("unexpected error: %v", err)25 }2627 if result != tt.expected {28 t.Errorf("Divide(%v, %v) = %v; want %v",29 tt.a, tt.b, result, tt.expected)30 }31 })32 }33}
Тестирование ошибок
1func TestCreateUser(t *testing.T) {2 tests := []struct {3 name string4 input CreateUserRequest5 wantErr error6 }{7 {8 name: "valid user",9 input: CreateUserRequest{Name: "Alice", Email: "alice@test.com"},10 wantErr: nil,11 },12 {13 name: "empty name",14 input: CreateUserRequest{Name: "", Email: "alice@test.com"},15 wantErr: ErrEmptyName,16 },17 }1819 for _, tt := range tests {20 t.Run(tt.name, func(t *testing.T) {21 _, err := CreateUser(tt.input)22 if !errors.Is(err, tt.wantErr) {23 t.Errorf("got error %v, want %v", err, tt.wantErr)24 }25 })26 }27}
Моки и интерфейсы
▸Определение интерфейса
1// repository.go2type UserRepository interface {3 GetByID(id int) (*User, error)4 Create(user *User) error5 Update(user *User) error6 Delete(id int) error7}89// Mock реализация10type MockUserRepository struct {11 Users map[int]*User12}1314func NewMockUserRepository() *MockUserRepository {15 return &MockUserRepository{Users: make(map[int]*User)}16}1718func (m *MockUserRepository) GetByID(id int) (*User, error) {19 user, ok := m.Users[id]20 if !ok {21 return nil, ErrUserNotFound22 }23 return user, nil24}2526func (m *MockUserRepository) Create(user *User) error {27 m.Users[user.ID] = user28 return nil29}
▸Тестирование с моком
1func TestUserService_GetUser(t *testing.T) {2 repo := NewMockUserRepository()3 repo.Users[1] = &User{ID: 1, Name: "Alice"}45 service := NewUserService(repo)67 t.Run("found", func(t *testing.T) {8 user, err := service.GetUser(1)9 if err != nil {10 t.Fatalf("unexpected error: %v", err)11 }12 if user.Name != "Alice" {13 t.Errorf("got name %s, want Alice", user.Name)14 }15 })1617 t.Run("not found", func(t *testing.T) {18 _, err := service.GetUser(999)19 if err != ErrUserNotFound {20 t.Errorf("got error %v, want ErrUserNotFound", err)21 }22 })23}
Benchmarks
1func BenchmarkAdd(b *testing.B) {2 for i := 0; i < b.N; i++ {3 Add(2, 3)4 }5}67func BenchmarkConcatenation(b *testing.B) {8 for i := 0; i < b.N; i++ {9 s := ""10 for j := 0; j < 1000; j++ {11 s += "a"12 }13 }14}1516func BenchmarkStringBuilder(b *testing.B) {17 for i := 0; i < b.N; i++ {18 var builder strings.Builder19 for j := 0; j < 1000; j++ {20 builder.WriteString("a")21 }22 _ = builder.String()23 }24}
1# Запуск benchmarks2go test -bench=. ./...34# С памятью5go test -bench=. -benchmem ./...67# Конкретный benchmark8go test -bench=BenchmarkAdd ./...
Интеграционные тесты
1//go:build integration23package main45import (6 "testing"7 "database/sql"8 _ "github.com/lib/pq"9)1011func TestIntegration_CreateUser(t *testing.T) {12 db, _ := sql.Open("postgres", testConnStr)13 defer db.Close()1415 // Создание пользователя16 id, err := CreateUser(db, "Test", "test@test.com")17 if err != nil {18 t.Fatalf("failed to create user: %v", err)19 }2021 // Проверка22 user, err := GetUser(db, id)23 if err != nil {24 t.Fatalf("failed to get user: %v", err)25 }2627 if user.Name != "Test" {28 t.Errorf("got name %s, want Test", user.Name)29 }30}
Best Practices
▸Именование тестов
1// Плохо2func TestSomething(t *testing.T) {}34// Хорошо5func TestAdd_ReturnsSumOfTwoNumbers(t *testing.T) {}6func TestDivide_ReturnsErrorOnZeroDivisor(t *testing.T) {}
▸Тестирование граничных случаев
1func TestAdd_EdgeCases(t *testing.T) {2 tests := []struct {3 name string4 a, b int5 expected int6 }{7 {"max int overflow", math.MaxInt, 1, math.MinInt}, // Ожидаем overflow8 {"min int", math.MinInt, 0, math.MinInt},9 }10 // ...11}
Заключение
Тестирование — обязательная часть разработки на Go. Table-driven tests — идиоматический способ организации тестов. Важно тестировать не только happy path, но и ошибки, граничные случаи. На собеседовании спрашивают про паттерны тестирования, моки через интерфейсы и benchmarks.