Тестирование
Тесты встроены в язык
Одно из преимуществ Go — тестирование идёт «из коробки». Не нужен внешний фреймворк. Стандартный пакет testing + команда go test — всё что нужно.
myapp/
├── user.go // package user
└── user_test.go // package user (или package user_test)
Правила тестовых файлов:
- Имя заканчивается на
_test.go - Тестовые функции начинаются с
Test - Принимают
*testing.T
go test ./... # все тесты в проекте
go test ./user/... # тесты в пакете user
go test -v ./... # с подробным выводом
go test -run TestUser # только тесты matching "TestUser"Первый тест
// user.go
package user
func FullName(first, last string) string {
return first + " " + last
}// user_test.go
package user
import "testing"
func TestFullName(t *testing.T) {
result := FullName("Иван", "Иванов")
expected := "Иван Иванов"
if result != expected {
t.Errorf("FullName() = %q, хотели %q", result, expected)
}
}$ go test
PASS
ok myapp/user 0.002s
$ go test -v
=== RUN TestFullName
--- PASS: TestFullName (0.00s)
PASSTable-driven tests: идиоматичный Go
Вместо того чтобы писать отдельный тест для каждого случая — собираем все в таблицу:
func TestFullName(t *testing.T) {
tests := []struct {
name string
first string
last string
expected string
}{
{
name: "обычный случай",
first: "Иван",
last: "Иванов",
expected: "Иван Иванов",
},
{
name: "пустое имя",
first: "",
last: "Иванов",
expected: " Иванов",
},
{
name: "только имя",
first: "Иван",
last: "",
expected: "Иван ",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := FullName(tc.first, tc.last)
if result != tc.expected {
t.Errorf("FullName(%q, %q) = %q, хотели %q",
tc.first, tc.last, result, tc.expected)
}
})
}
}t.Run создаёт подтест — можно запустить конкретный случай:
go test -run "TestFullName/пустое_имя"Провальный подтест не останавливает остальные — видишь все ошибки сразу.
t.Fatal vs t.Error
func TestSomething(t *testing.T) {
user, err := createUser("test@example.com")
// Fatal: при ошибке тест останавливается немедленно
// Используй когда дальнейшие проверки бессмысленны без этого значения
if err != nil {
t.Fatalf("createUser() вернул ошибку: %v", err)
}
// Error: тест продолжается, собираем все ошибки
if user.Email != "test@example.com" {
t.Errorf("Email = %q, хотели %q", user.Email, "test@example.com")
}
if user.ID == 0 {
t.Error("ID не должен быть 0")
}
}Правило: Fatal когда следующие проверки используют возвращённое значение (nil pointer → паника). Error когда проверки независимы.
Вспомогательные функции тестов
// helper помечает функцию как вспомогательную —
// в стектрейсе будет показана строка вызывающего, не helper
func assertUser(t *testing.T, got, want *User) {
t.Helper() // важно!
if got.Email != want.Email {
t.Errorf("Email: got %q, want %q", got.Email, want.Email)
}
if got.Name != want.Name {
t.Errorf("Name: got %q, want %q", got.Name, want.Name)
}
}
func TestCreateUser(t *testing.T) {
user, _ := createUser("test@example.com", "Тест")
assertUser(t, user, &User{Email: "test@example.com", Name: "Тест"})
// При ошибке покажет строку ЗДЕСЬ, а не внутри assertUser
}Бенчмарки
func BenchmarkFullName(b *testing.B) {
for i := 0; i < b.N; i++ {
FullName("Иван", "Иванов")
}
}go test -bench=. -benchmem
# BenchmarkFullName-8 50000000 25.3 ns/op 16 B/op 1 allocs/opb.N — количество итераций, Go подбирает автоматически для стабильного результата. -benchmem показывает аллокации.
Бенчмарк с подготовкой:
func BenchmarkSort(b *testing.B) {
data := generateLargeSlice(10000) // подготовка
b.ResetTimer() // не считаем подготовку
for i := 0; i < b.N; i++ {
sort.Ints(data)
}
}Покрытие кода
# Показать процент покрытия
go test -cover ./...
# Сохранить профиль покрытия
go test -coverprofile=coverage.out ./...
# Открыть HTML отчёт в браузере
go tool cover -html=coverage.outHTML отчёт подсвечивает строки: зелёный — покрыто, красный — нет. Полезно найти непротестированные edge cases.
100% покрытие — не цель. Фокусируйся на критической бизнес-логике.
testify: популярная библиотека утверждений
Стандартный testing минималистичен. github.com/stretchr/testify добавляет удобные assert:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUser(t *testing.T) {
user, err := createUser("test@test.com")
// require — аналог t.Fatal (останавливает тест при ошибке)
require.NoError(t, err)
require.NotNil(t, user)
// assert — аналог t.Error (продолжает тест)
assert.Equal(t, "test@test.com", user.Email)
assert.Greater(t, user.ID, 0)
assert.True(t, user.IsActive)
}Сообщения об ошибках из testify намного информативнее стандартных. В реальных проектах почти всегда используют testify.
Тестирование с зависимостями: интерфейсы и моки
Хороший код тестируется через интерфейсы:
// Интерфейс для зависимости
type UserRepository interface {
GetByID(id int) (*User, error)
Save(u *User) error
}
// Мок для тестов
type mockRepo struct {
users map[int]*User
}
func (m *mockRepo) GetByID(id int) (*User, error) {
u, ok := m.users[id]
if !ok {
return nil, ErrNotFound
}
return u, nil
}
func TestService(t *testing.T) {
repo := &mockRepo{users: map[int]*User{
1: {ID: 1, Name: "Тест"},
}}
svc := NewUserService(repo) // внедряем мок
user, err := svc.GetUser(1)
require.NoError(t, err)
assert.Equal(t, "Тест", user.Name)
}Это намного быстрее чем тесты с реальной БД и работает без инфраструктуры.
func Add(a, b int) int {
return a + b
}package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1,2)=%d, хотели %d",
result, expected)
}
}