- Home »

Как писать юнит-тесты в Go с помощью go test и пакета testing
Если ты серьёзно относишься к разработке и запуску сервисов в production, то тебе просто не обойтись без качественных юнит-тестов. Особенно если работаешь с Go — языком, который изначально создавался с мыслью о надёжности и простоте. Сегодня разберём, как правильно писать unit-тесты в Go с помощью встроенного пакета testing и инструмента go test. Это не теория ради теории — это must-have навык для любого, кто деплоит код на серверы и не хочет просыпаться ночью от аварийных уведомлений.
Зачем это нужно? Всё просто: тесты — это твоя страховка от багов в production. Они помогают быстро находить проблемы при рефакторинге, дают уверенность в работе кода и существенно экономят время на отладку. Плюс, если используешь CI/CD пайплайны на своих серверах, хорошие тесты станут основой для автоматизации деплоя.
Как это работает: основы go test
Go подходит к тестированию максимально прагматично. Никаких внешних зависимостей — всё встроено в язык. Пакет testing предоставляет простой API для написания тестов, а команда go test умеет их находить и запускать.
Основные принципы работы:
- Тестовые файлы должны заканчиваться на
_test.go
- Функции тестов начинаются с
Test
и принимают параметр*testing.T
- Для бенчмарков используй префикс
Benchmark
и параметр*testing.B
- Тесты живут в том же пакете, что и код
Вот простейший пример:
// math.go
package main
func Add(a, b int) int {
return a + b
}
func Multiply(a, b int) int {
return a * b
}
// math_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; expected %d", result, expected)
}
}
func TestMultiply(t *testing.T) {
result := Multiply(4, 5)
expected := 20
if result != expected {
t.Errorf("Multiply(4, 5) = %d; expected %d", result, expected)
}
}
Пошаговая настройка и запуск
Начнём с самого простого. Создаём проект и пишем первый тест:
# Создаём директорию проекта
mkdir go-testing-example
cd go-testing-example
# Инициализируем модуль
go mod init testing-example
# Создаём файл с кодом
touch calculator.go
# Создаём файл с тестами
touch calculator_test.go
Теперь добавляем код в calculator.go
:
package main
import (
"errors"
"math"
)
type Calculator struct{}
func (c *Calculator) Add(a, b float64) float64 {
return a + b
}
func (c *Calculator) Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func (c *Calculator) Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, errors.New("negative number")
}
return math.Sqrt(x), nil
}
И тесты в calculator_test.go
:
package main
import (
"testing"
)
func TestCalculator_Add(t *testing.T) {
calc := &Calculator{}
tests := []struct {
name string
a, b float64
expected float64
}{
{"positive numbers", 2.5, 3.5, 6.0},
{"negative numbers", -1.0, -2.0, -3.0},
{"zero", 0, 5.0, 5.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calc.Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%f, %f) = %f; expected %f",
tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestCalculator_Divide(t *testing.T) {
calc := &Calculator{}
// Тест успешного деления
result, err := calc.Divide(10.0, 2.0)
if err != nil {
t.Errorf("Divide(10.0, 2.0) returned error: %v", err)
}
if result != 5.0 {
t.Errorf("Divide(10.0, 2.0) = %f; expected 5.0", result)
}
// Тест деления на ноль
_, err = calc.Divide(10.0, 0.0)
if err == nil {
t.Error("Divide(10.0, 0.0) should return error")
}
}
func TestCalculator_Sqrt(t *testing.T) {
calc := &Calculator{}
// Позитивный тест
result, err := calc.Sqrt(9.0)
if err != nil {
t.Errorf("Sqrt(9.0) returned error: %v", err)
}
if result != 3.0 {
t.Errorf("Sqrt(9.0) = %f; expected 3.0", result)
}
// Негативный тест
_, err = calc.Sqrt(-1.0)
if err == nil {
t.Error("Sqrt(-1.0) should return error")
}
}
Основные команды go test
Вот полный набор команд, которые понадобятся в работе:
# Запуск всех тестов в текущем пакете
go test
# Запуск тестов с детальным выводом
go test -v
# Запуск тестов в конкретном файле
go test -v calculator_test.go calculator.go
# Запуск конкретного теста
go test -v -run TestCalculator_Add
# Запуск тестов с покрытием кода
go test -cover
# Генерация HTML отчёта по покрытию
go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
# Запуск тестов во всех подпакетах
go test ./...
# Запуск тестов в параллельном режиме
go test -parallel 4
# Запуск с таймаутом
go test -timeout 30s
# Бенчмарки
go test -bench=.
# Профилирование CPU
go test -cpuprofile=cpu.prof -bench=.
# Профилирование памяти
go test -memprofile=mem.prof -bench=.
Продвинутые техники тестирования
Когда базовые навыки освоены, можно переходить к более сложным вещам. Разберём несколько полезных паттернов:
Table-driven тесты
Это классика Go. Позволяет тестировать множество сценариев в одной функции:
func TestStringValidator(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{"valid email", "user@example.com", true},
{"invalid email", "invalid-email", false},
{"empty string", "", false},
{"too long", strings.Repeat("a", 1000), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsValidEmail(tt.input)
if result != tt.expected {
t.Errorf("IsValidEmail(%q) = %v; expected %v",
tt.input, result, tt.expected)
}
})
}
}
Мокирование и зависимости
Для работы с внешними зависимостями используй интерфейсы:
// Интерфейс для базы данных
type UserStore interface {
GetUser(id int) (*User, error)
SaveUser(*User) error
}
// Реальная реализация
type PostgresStore struct {
db *sql.DB
}
func (p *PostgresStore) GetUser(id int) (*User, error) {
// Реальная работа с базой
return nil, nil
}
// Мок для тестов
type MockUserStore struct {
users map[int]*User
}
func (m *MockUserStore) GetUser(id int) (*User, error) {
user, exists := m.users[id]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserStore) SaveUser(user *User) error {
m.users[user.ID] = user
return nil
}
// Тест с моком
func TestUserService_GetUser(t *testing.T) {
mockStore := &MockUserStore{
users: map[int]*User{
1: {ID: 1, Name: "John"},
},
}
service := &UserService{store: mockStore}
user, err := service.GetUser(1)
if err != nil {
t.Errorf("GetUser(1) returned error: %v", err)
}
if user.Name != "John" {
t.Errorf("GetUser(1) returned name %s; expected John", user.Name)
}
}
Тестирование HTTP-хандлеров
Для веб-сервисов Go предоставляет удобные инструменты:
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(HealthHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := `{"status":"ok"}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
Бенчмарки для оптимизации
Бенчмарки помогают измерить производительность кода и найти узкие места:
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
var result string
for j := 0; j < 1000; j++ {
result += "a"
}
}
}
func BenchmarkStringBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var builder strings.Builder
for j := 0; j < 1000; j++ {
builder.WriteString("a")
}
_ = builder.String()
}
}
func BenchmarkParallelWork(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Какая-то CPU-интенсивная работа
for i := 0; i < 10000; i++ {
_ = i * i
}
}
})
}
Запуск бенчмарков:
# Запуск всех бенчмарков
go test -bench=.
# Запуск конкретного бенчмарка
go test -bench=BenchmarkStringConcat
# Бенчмарк с профилированием памяти
go test -bench=. -memprofile=mem.prof
# Сравнение производительности
go test -bench=. -count=5 | tee bench.txt
Сравнение с другими решениями
Решение | Плюсы | Минусы | Когда использовать |
---|---|---|---|
Стандартный testing | Встроен в язык, простой, быстрый | Базовая функциональность | Для большинства случаев |
Testify | Удобные assert'ы, моки | Внешняя зависимость | Для сложных тестов |
Ginkgo + Gomega | BDD-стиль, много возможностей | Сложность, overhead | Для больших проектов |
GoConvey | Веб-интерфейс, живые тесты | Избыточность для простых случаев | Для разработки с интерфейсом |
Интеграция с CI/CD
Если деплоишь код на VPS или выделенный сервер, то автоматизация тестирования — это святое. Вот пример конфигурации для различных CI систем:
# .github/workflows/test.yml (GitHub Actions)
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.21
- name: Run tests
run: |
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
file: ./coverage.txt
# .gitlab-ci.yml
test:
image: golang:1.21
script:
- go mod download
- go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- go tool cover -func=coverage.txt
coverage: '/total:\s+\(statements\)\s+(\d+\.\d+)%/'
Нестандартные способы использования
Пакет testing можно использовать не только для unit-тестов. Вот несколько интересных приёмов:
Интеграционные тесты
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
// Запуск внешних сервисов, тестирование API
db := setupTestDB(t)
defer teardownTestDB(t, db)
// Тесты с реальной базой данных
}
Фаззинг (Go 1.18+)
func FuzzParseURL(f *testing.F) {
f.Add("http://example.com")
f.Add("https://example.com/path")
f.Add("ftp://example.com")
f.Fuzz(func(t *testing.T, url string) {
parsed, err := ParseURL(url)
if err != nil {
return // Ошибка разбора - это нормально
}
// Проверяем инварианты
if parsed.Host == "" && parsed.Scheme != "" {
t.Errorf("URL with scheme should have host: %s", url)
}
})
}
Генерация тестовых данных
func TestMain(m *testing.M) {
// Подготовка к тестам
setup()
// Запуск тестов
code := m.Run()
// Очистка после тестов
cleanup()
os.Exit(code)
}
Полезные пакеты и инструменты
- Testify — удобные assertions и моки
- GoMock — генерация моков по интерфейсам
- sqlmock — мокирование SQL-запросов
- httpmock — мокирование HTTP-запросов
- Mockery — альтернативный генератор моков
Скрипты для автоматизации
Создай Makefile для упрощения работы с тестами:
# Makefile
.PHONY: test test-verbose test-race test-cover test-bench clean
test:
go test ./...
test-verbose:
go test -v ./...
test-race:
go test -race ./...
test-cover:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
test-bench:
go test -bench=. -benchmem ./...
test-integration:
go test -tags=integration ./...
clean:
rm -f coverage.out coverage.html *.prof
# Запуск с различными настройками
test-ci:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
go tool cover -func=coverage.txt
Статистика и мониторинг тестов
Для серьёзных проектов важно следить за метриками тестов:
# Скрипт для анализа тестов
#!/bin/bash
echo "=== Test Coverage Report ==="
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out | tail -1
echo "=== Test Performance ==="
go test -bench=. -benchtime=5s ./... | grep -E "(Benchmark|PASS|FAIL)"
echo "=== Test Count ==="
find . -name "*_test.go" -exec grep -l "func Test" {} \; | wc -l
echo "=== Failed Tests History ==="
go test -json ./... | jq -r 'select(.Action == "fail") | .Test'
Заключение и рекомендации
Тестирование в Go — это не просто good practice, а необходимость для любого серьёзного проекта. Встроенный пакет testing покрывает 90% потребностей, а для остальных 10% есть отличные сторонние решения.
Основные рекомендации:
- Начинай с простых unit-тестов на критичную логику
- Используй table-driven тесты для проверки множества сценариев
- Не забывай про тесты ошибок — они часто выявляют проблемы
- Интегрируй тесты в CI/CD pipeline с самого начала
- Стремись к покрытию 70-80% — больше часто не оправдано
- Бенчмарки обязательны для performance-критичного кода
Помни: хорошие тесты — это инвестиция в будущее. Они сэкономят тебе массу времени на отладку и дадут уверенность при деплое на production серверы. Начни с малого, но начни сегодня — твоё будущее "я" скажет спасибо.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.