Модуль
1Пакеты: основы2Go модули3Тестирование← вы здесь4Инструменты Go
Урок 3~12 минут

Тестирование

Тесты встроены в язык

Одно из преимуществ Go — тестирование идёт «из коробки». Не нужен внешний фреймворк. Стандартный пакет testing + команда go test — всё что нужно.

myapp/
├── user.go       // package user
└── user_test.go  // package user (или package user_test)

Правила тестовых файлов:

  • Имя заканчивается на _test.go
  • Тестовые функции начинаются с Test
  • Принимают *testing.T
bash
go test ./...           # все тесты в проекте
go test ./user/...      # тесты в пакете user
go test -v ./...        # с подробным выводом
go test -run TestUser   # только тесты matching "TestUser"

Первый тест

go
// user.go
package user
 
func FullName(first, last string) string {
    return first + " " + last
}
go
// 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)
    }
}
bash
$ go test
PASS
ok  myapp/user  0.002s
 
$ go test -v
=== RUN   TestFullName
--- PASS: TestFullName (0.00s)
PASS

Table-driven tests: идиоматичный Go

Вместо того чтобы писать отдельный тест для каждого случая — собираем все в таблицу:

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 создаёт подтест — можно запустить конкретный случай:

bash
go test -run "TestFullName/пустое_имя"

Провальный подтест не останавливает остальные — видишь все ошибки сразу.


t.Fatal vs t.Error

go
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 когда проверки независимы.


Вспомогательные функции тестов

go
// 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
}

Бенчмарки

go
func BenchmarkFullName(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FullName("Иван", "Иванов")
    }
}
bash
go test -bench=. -benchmem
# BenchmarkFullName-8   50000000   25.3 ns/op   16 B/op   1 allocs/op

b.N — количество итераций, Go подбирает автоматически для стабильного результата. -benchmem показывает аллокации.

Бенчмарк с подготовкой:

go
func BenchmarkSort(b *testing.B) {
    data := generateLargeSlice(10000)  // подготовка
    b.ResetTimer()                      // не считаем подготовку
 
    for i := 0; i < b.N; i++ {
        sort.Ints(data)
    }
}

Покрытие кода

bash
# Показать процент покрытия
go test -cover ./...
 
# Сохранить профиль покрытия
go test -coverprofile=coverage.out ./...
 
# Открыть HTML отчёт в браузере
go tool cover -html=coverage.out

HTML отчёт подсвечивает строки: зелёный — покрыто, красный — нет. Полезно найти непротестированные edge cases.

100% покрытие — не цель. Фокусируйся на критической бизнес-логике.


testify: популярная библиотека утверждений

Стандартный testing минималистичен. github.com/stretchr/testify добавляет удобные assert:

go
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.


Тестирование с зависимостями: интерфейсы и моки

Хороший код тестируется через интерфейсы:

go
// Интерфейс для зависимости
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)
}

Это намного быстрее чем тесты с реальной БД и работает без инфраструктуры.

Тестовые файлы — *_test.go, функции — Test*(t *testing.T). go test ./... запускает все тесты. Табличные тесты — идиоматичный Go: один цикл вместо дублирования кода.
add.go
func Add(a, b int) int {
    return a + b
}
add_test.go
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)
    }
}
🎯
Миссия 1 из 4
Какое соглашение об именовании файлов с тестами в Go?