Почему тестирование в Go — это не страшно, а необходимо
Когда вы впервые пишете на Go, кажется, что компилятор ловит почти всё: типы строгие, ошибки явные, nil под контролем. Но стоит вашему проекту вырасти до 500+ строк, как вы понимаете: без тестов жить нельзя. Одна правка в одном месте ломает другое, а вы узнаёте об этом только в продакшне.
В этой статье я покажу, с чего начать тестирование на Go начинающему разработчику. Мы разберёмся с юнит-тестами, табличными тестами, моками и покрытием кода. Никакой воды — только рабочие примеры и практические советы.
Если вы пишете на Go хотя бы пару месяцев, этот материал сэкономит вам часы отладки.
Что нужно знать перед стартом
Go предоставляет встроенный пакет testing — ничего дополнительно ставить не нужно. Тесты пишутся в файлах с суффиксом _test.go и запускаются командой:
go test ./...
Основные правила:
- Функция теста должна называться
TestXxx(t *testing.T), где Xxx — имя тестируемой функции с большой буквы. - Файлы тестов лежат рядом с тестируемым кодом.
- Ошибки сообщаются через
t.Error()илиt.Fatal().
Давайте сразу перейдём к практике.
Пишем первый юнит-тест на Go
Допустим, у нас есть простая функция, которая суммирует два числа:
// calc.go
package calc
func Sum(a, b int) int {
return a + b
}
Создаём файл calc_test.go в той же папке:
// calc_test.go
package calc
import "testing"
func TestSum(t *testing.T) {
result := Sum(2, 3)
expected := 5
if result != expected {
t.Errorf("Sum(2,3) = %d; want %d", result, expected)
}
}
Запускаем тест:
go test -v
Вы увидите зелёный PASS — ваша первая тестовая победа. Обратите внимание: мы используем t.Errorf (не фатальный) — тест продолжит работу, даже если один кейс упал.
Табличные тесты: когда один тест неудобен
Представьте, что нужно проверить функцию с десятком разных входных данных. Писать 10 отдельных тестов? Нет, в Go для этого есть табличные тесты (table-driven tests). Это стандарт для Go-сообщества.
Добавим в наш calc.go функцию деления:
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Теперь напишем табличный тест:
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
wantErr bool
}{
{"positive", 10, 2, 5, false},
{"negative", -6, 3, -2, false},
{"zero division", 5, 0, 0, true},
{"same numbers", 7, 7, 1, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if got != tt.expected {
t.Errorf("Divide(%d,%d) = %d; want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
Что здесь крутого:
- Каждый кейс — отдельная подтест с именем. Если один упадёт, остальные выполнятся.
- Легко добавлять новые случаи — просто дописываете строку в структуру.
- Код читается как спецификация.
Запустите с флагом -v — увидите имена всех подтестов. Это очень удобно при отладке.
Тестирование HTTP-обработчиков
Веб-приложения на Go — классика. Давайте протестируем простой HTTP-хендлер без запуска реального сервера. Go позволяет создать тестовый HTTP-запрос через httptest.NewRecorder.
Напишем обработчик, который возвращает JSON с приветствием:
// handler.go
package handler
import (
"encoding/json"
"net/http"
)
type Greeting struct {
Message string `json:"message"`
}
func HelloHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
resp := Greeting{Message: "Hello, " + name + "!"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
Тест:
// handler_test.go
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHelloHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/hello?name=Go", nil)
w := httptest.NewRecorder()
HelloHandler(w, req)
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected 200, got %d", resp.StatusCode)
}
var greeting Greeting
json.NewDecoder(resp.Body).Decode(&greeting)
expected := "Hello, Go!"
if greeting.Message != expected {
t.Errorf("expected %q, got %q", expected, greeting.Message)
}
}
Преимущество: тест работает мгновенно, не нужно поднимать порты. Можно проверить статус, заголовки, тело ответа.
Моки (mocks) в Go: когда тестировать нужно с внешними зависимостями
Ваш код почти наверняка обращается к базе данных, API или файловой системе. В тестах мы не хотим зависеть от реальных сервисов — они медленные, ненадёжные и требуют настройки. Здесь помогают моки.
Лучший способ в Go — использовать интерфейсы. Если функция принимает интерфейс, вы можете подставить заглушку в тесте.
Пример: сохраняем пользователя в базу.
// user.go
type User struct {
ID int
Name string
}
type UserRepository interface {
Save(user User) error
}
func CreateUser(repo UserRepository, name string) (User, error) {
user := User{Name: name}
err := repo.Save(user)
return user, err
}
В тесте создаём простой мок:
// user_test.go
type mockRepo struct {
savedUser User
shouldFail bool
}
func (m *mockRepo) Save(user User) error {
if m.shouldFail {
return errors.New("db error")
}
m.savedUser = user
return nil
}
func TestCreateUser(t *testing.T