В этом курсе вы шаг за шагом познакомились с тестированием в Go. Мы начали с самых основ — простых unit-тестов, проверки ошибок и паник, а затем постепенно углублялись: разбирали табличные тесты, вспомогательные функции и интерфейс testing.TB
, работу с временными файлами и каталогами, создание моков через интерфейсы и библиотеку testify, организацию параллельных тестов и защиту от гонок, измерение покрытия кода. В финале мы вышли на лучшие практики, которые помогают писать не только работающие, но и понятные, поддерживаемые тесты.
На этом пути вы увидели, что тесты — это не дополнительная нагрузка, а важная часть разработки. Они защищают код от регрессий, позволяют смелее вносить изменения, документируют поведение функций и систем. Go предлагает встроенные средства для тестирования, которые можно дополнять внешними библиотеками, если проект становится крупнее и требует удобства.
Главная идея курса проста: тесты должны быть не только формальной проверкой, но и понятным рассказом о том, как работает ваш код. Когда тест написан ясно, его можно читать как документацию. Когда тест изолирован, он не мешает другим и показывает достоверный результат. Когда тесты охватывают разные сценарии, они превращаются в сетку безопасности, которая позволяет двигаться вперёд без страха сломать систему.
В реальной разработке тестирование — это не цель само по себе, а инструмент. Оно помогает ускорять команду, улучшать архитектуру и упрощать жизнь. Чем понятнее и надёжнее тесты, тем увереннее можно работать с кодовой базой, особенно когда она растёт.
Теперь у вас есть всё необходимое, чтобы писать тесты в Go профессионально: от самых базовых проверок до сложных сценариев с моками, параллельностью и анализом покрытия. Дальше всё зависит от практики. Чем больше вы будете писать тестов в реальных проектах, тем увереннее станете применять приёмы, которые мы рассмотрели, и тем надёжнее станет ваш код.
Самостоятельная работа
Завершаем блок практикой по testify: быстро отлавливаем ошибки и проверяем детали.
Что сделать
Реализуйте
Div(a, b int) (int, error)
, которая падает с ошибкой при делении на ноль.Пример реализации
package calc import "errors" var ErrZeroDivisor = errors.New("division by zero") func Div(a, b int) (int, error) { if b == 0 { return 0, ErrZeroDivisor } return a / b, nil }
Напишите тесты с
require
(останавливает тест при ошибке) иassert
(сравнение значений).
Что покрыть в тестах
b == 0
: черезrequire.Error
иassert.Equal
для текста/значения ошибки.- Несколько корректных кейсов деления: положительные, отрицательные, нули в делимом.
Показать решение
package calc
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDiv_ErrorOnZero(t *testing.T) {
_, err := Div(10, 0)
require.Error(t, err)
assert.Equal(t, ErrZeroDivisor, err)
}
func TestDiv_Valid(t *testing.T) {
cases := []struct{ a, b, want int }{
{10, 2, 5},
{-9, 3, -3},
{0, 5, 0},
}
for _, c := range cases {
got, err := Div(c.a, c.b)
require.NoError(t, err)
assert.Equal(t, c.want, got)
}
}
Итоговое задание: сервис пользователей
Финальный штрих — маленький сервис поверх абстрактного хранилища.
Что сделать
- Реализуйте добавление и получение по ID через интерфейс
UserStorage
(ниже). - Напишите тесты:
- С реальной реализацией на
map
(простая память). - С моками на testify/mock.
- С реальной реализацией на
package users
import "errors"
type User struct {
ID int
Name string
}
var ErrNotFound = errors.New("user not found")
type UserStorage interface {
Save(u User) error
ByID(id int) (User, error)
}
type Service struct{ s UserStorage }
func New(s UserStorage) *Service { return &Service{s: s} }
func (svc *Service) Add(u User) error { return svc.s.Save(u) }
func (svc *Service) Get(id int) (User, error) { return svc.s.ByID(id) }
Что покрыть в тестах
- Добавление и получение существующего пользователя.
- Поведение для несуществующего ID (ошибка
ErrNotFound
). - Для мука: проверка, что методы были вызваны с ожидаемыми аргументами.
Показать решение
package users
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Реальная память на map
type memStore struct{ m map[int]User }
func (ms *memStore) Save(u User) error {
if ms.m == nil {
ms.m = make(map[int]User)
}
ms.m[u.ID] = u
return nil
}
func (ms *memStore) ByID(id int) (User, error) {
if u, ok := ms.m[id]; ok {
return u, nil
}
return User{}, ErrNotFound
}
func TestService_WithMemStore(t *testing.T) {
svc := New(&memStore{})
u := User{ID: 1, Name: "Hexlet"}
assert.NoError(t, svc.Add(u))
got, err := svc.Get(1)
assert.NoError(t, err)
assert.Equal(t, u, got)
_, err = svc.Get(2)
assert.Error(t, err)
assert.Equal(t, ErrNotFound, err)
}
// Мок на testify/mock
type mockStore struct{ mock.Mock }
func (m *mockStore) Save(u User) error {
args := m.Called(u)
return args.Error(0)
}
func (m *mockStore) ByID(id int) (User, error) {
args := m.Called(id)
return args.Get(0).(User), args.Error(1)
}
func TestService_WithMock(t *testing.T) {
ms := &mockStore{}
svc := New(ms)
u := User{ID: 7, Name: "Gopher"}
ms.On("Save", u).Return(nil).Once()
ms.On("ByID", 7).Return(u, nil).Once()
assert.NoError(t, svc.Add(u))
got, err := svc.Get(7)
assert.NoError(t, err)
assert.Equal(t, u, got)
ms.AssertExpectations(t)
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.