jest / vitest → go test + пакет testing
describe/it → func TestXxx(t *testing.T)
expect(value).toBe(expected) → assert.Equal(t, expected, value)
jest.mock('./module') → gomock (генерирует моки интерфейсов)
beforeEach/afterEach → setup/teardown вручную
jest --coverage → go test -cover
testing — основа всех тестов в Go
-coverprofilet.Parallel()mkdir go-testing && cd go-testing
go mod init go-testing
# Устанавливаем зависимости
go get github.com/stretchr/testify
go get github.com/golang/mock/gomock
go get github.com/google/uuid
go mod tidy
# Устанавливаем mockgen (генератор моков)
go install github.com/golang/mock/mockgen@latest
# Создаём структуру
mkdir -p internal/user
mkdir -p internal/service
mkdir -p mocks
internal/user/user.gopackage user
import (
"context"
"errors"
"fmt"
"strings"
)
// User — доменная модель
type User struct {
ID string
Email string
Name string
}
// Repository — ИНТЕРФЕЙС (его будем мокать)
type Repository interface {
Create(ctx context.Context, u *User) error
GetByID(ctx context.Context, id string) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
}
// Ошибки
var (
ErrNotFound = errors.New("user not found")
ErrAlreadyExists = errors.New("user already exists")
ErrEmptyEmail = errors.New("email must not be empty")
ErrEmptyName = errors.New("name must not be empty")
)
internal/service/user_service.gopackage service
import (
"context"
"fmt"
"strings"
"go-testing/internal/user"
"github.com/google/uuid"
)
// UserService — бизнес-логика (её будем тестировать!)
type UserService struct {
repo user.Repository
}
func NewUserService(repo user.Repository) *UserService {
return &UserService{repo: repo}
}
// Register — регистрация пользователя
func (s *UserService) Register(ctx context.Context, email, name string) (*user.User, error) {
// Валидация
if strings.TrimSpace(email) == "" {
return nil, user.ErrEmptyEmail
}
if !strings.Contains(email, "@") {
return nil, fmt.Errorf("invalid email format")
}
if strings.TrimSpace(name) == "" {
return nil, user.ErrEmptyName
}
// Проверка на дубликат
existing, err := s.repo.GetByEmail(ctx, email)
if err != nil && !errors.Is(err, user.ErrNotFound) {
return nil, fmt.Errorf("check email: %w", err)
}
if existing != nil {
return nil, user.ErrAlreadyExists
}
// Создаём пользователя
u := &user.User{
ID: uuid.New().String(),
Email: email,
Name: name,
}
if err := s.repo.Create(ctx, u); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return u, nil
}
// GetUser — получение пользователя
func (s *UserService) GetUser(ctx context.Context, id string) (*user.User, error) {
if id == "" {
return nil, fmt.Errorf("id must not be empty")
}
return s.repo.GetByID(ctx, id)
}
// IsValidEmail — чистая функция (легко тестировать)
func IsValidEmail(email string) bool {
return strings.Contains(email, "@") && len(email) > 5
}
import "errors"
# Генерируем мок для интерфейса user.Repository
mockgen -source=internal/user/user.go \
-destination=mocks/mock_user_repository.go \
-package=mocks
# Или по import-пути:
# mockgen go-testing/internal/user Repository > mocks/mock_user_repository.go
internal/service/user_service_test.gopackage service_test
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go-testing/internal/service"
"go-testing/internal/user"
"go-testing/mocks"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 1. ТЕСТ: ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ║
// ╚══════════════════════════════════════════════════════════╝
// TestIsValidEmail — тест чистой функции (без моков)
func TestIsValidEmail(t *testing.T) {
// Table-driven test — идиоматичный паттерн Go
tests := []struct {
name string // Имя тест-кейса
email string // Входные данные
expected bool // Ожидаемый результат
}{
{"valid email", "alice@example.com", true},
{"valid with plus", "alice+tag@example.com", true},
{"no at sign", "aliceexample.com", false},
{"empty string", "", false},
{"short email", "a@b", false}, // len = 3, меньше 5
{"only at", "@", false},
}
for _, tt := range tests {
// t.Run создаёт подтест (видно в выводе)
t.Run(tt.name, func(t *testing.T) {
result := service.IsValidEmail(tt.email)
// testify/assert — мягкая проверка (тест продолжается)
assert.Equal(t, tt.expected, result,
"IsValidEmail(%q) should be %v", tt.email, tt.expected)
})
}
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 2. ТЕСТ: РЕГИСТРАЦИЯ С МОКАМИ ║
// ╚══════════════════════════════════════════════════════════╝
func TestUserService_Register_Success(t *testing.T) {
// Arrange — подготавливаем моки
ctrl := gomock.NewController(t)
defer ctrl.Finish() // Проверяет, что ВСЕ ожидания выполнены
mockRepo := mocks.NewMockRepository(ctrl)
svc := service.NewUserService(mockRepo)
ctx := context.Background()
// Настраиваем ожидания:
// 1. Проверка email — пользователь не найден
mockRepo.EXPECT().
GetByEmail(ctx, "alice@example.com").
Return(nil, user.ErrNotFound) // Нет такого — можно создавать
// 2. Создание пользователя
mockRepo.EXPECT().
Create(ctx, gomock.Any()). // gomock.Any() — любой аргумент
DoAndReturn(func(ctx context.Context, u *user.User) error {
// Проверяем, что поля заполнены
assert.Equal(t, "alice@example.com", u.Email)
assert.Equal(t, "Alice", u.Name)
assert.NotEmpty(t, u.ID)
return nil
})
// Act — выполняем тестируемый метод
u, err := svc.Register(ctx, "alice@example.com", "Alice")
// Assert — проверяем результат
require.NoError(t, err) // Жёсткая проверка (если ошибка — тест прерывается)
assert.NotNil(t, u)
assert.Equal(t, "alice@example.com", u.Email)
assert.Equal(t, "Alice", u.Name)
assert.NotEmpty(t, u.ID)
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 3. ТЕСТ: ОШИБКА ВАЛИДАЦИИ ║
// ╚══════════════════════════════════════════════════════════╝
func TestUserService_Register_ValidationErrors(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
svc := service.NewUserService(mockRepo)
ctx := context.Background()
tests := []struct {
name string
email string
userName string
expectErr error
}{
{"empty email", "", "Alice", user.ErrEmptyEmail},
{"empty name", "alice@example.com", "", user.ErrEmptyName},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Для валидационных ошибок мок не должен вызываться
// Если вызовется — gomock упадёт (нет ожиданий)
u, err := svc.Register(ctx, tt.email, tt.userName)
assert.Error(t, err)
assert.True(t, errors.Is(err, tt.expectErr),
"expected %v, got %v", tt.expectErr, err)
assert.Nil(t, u)
})
}
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 4. ТЕСТ: ДУБЛИКАТ EMAIL ║
// ╚══════════════════════════════════════════════════════════╝
func TestUserService_Register_DuplicateEmail(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
svc := service.NewUserService(mockRepo)
ctx := context.Background()
existingUser := &user.User{
ID: "existing-id",
Email: "alice@example.com",
Name: "Existing Alice",
}
// Ожидаем: проверка email — пользователь НАЙДЕН
mockRepo.EXPECT().
GetByEmail(ctx, "alice@example.com").
Return(existingUser, nil)
u, err := svc.Register(ctx, "alice@example.com", "Alice")
assert.Error(t, err)
assert.True(t, errors.Is(err, user.ErrAlreadyExists))
assert.Nil(t, u)
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 5. ТЕСТ: ОШИБКА РЕПОЗИТОРИЯ ║
// ╚══════════════════════════════════════════════════════════╝
func TestUserService_Register_RepositoryError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
svc := service.NewUserService(mockRepo)
ctx := context.Background()
dbError := errors.New("database connection lost")
// Проверка email — не найден
mockRepo.EXPECT().
GetByEmail(ctx, "alice@example.com").
Return(nil, user.ErrNotFound)
// Создание — ОШИБКА БД
mockRepo.EXPECT().
Create(ctx, gomock.Any()).
Return(dbError)
u, err := svc.Register(ctx, "alice@example.com", "Alice")
assert.Error(t, err)
assert.Contains(t, err.Error(), "create user")
assert.Contains(t, err.Error(), "database connection lost")
assert.Nil(t, u)
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 6. ТЕСТ: GetUser ║
// ╚══════════════════════════════════════════════════════════╝
func TestUserService_GetUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
svc := service.NewUserService(mockRepo)
ctx := context.Background()
t.Run("success", func(t *testing.T) {
expected := &user.User{ID: "123", Email: "alice@example.com", Name: "Alice"}
mockRepo.EXPECT().
GetByID(ctx, "123").
Return(expected, nil)
u, err := svc.GetUser(ctx, "123")
require.NoError(t, err)
assert.Equal(t, expected, u)
})
t.Run("not found", func(t *testing.T) {
mockRepo.EXPECT().
GetByID(ctx, "999").
Return(nil, user.ErrNotFound)
u, err := svc.GetUser(ctx, "999")
assert.Error(t, err)
assert.True(t, errors.Is(err, user.ErrNotFound))
assert.Nil(t, u)
})
t.Run("empty id", func(t *testing.T) {
u, err := svc.GetUser(ctx, "")
assert.Error(t, err)
assert.Nil(t, u)
})
}
internal/service/user_service_httptest_test.gopackage service_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ ТЕСТ HTTP-ОБРАБОТЧИКА (без поднятия сервера!) ║
// ╚══════════════════════════════════════════════════════════╝
// Пример хендлера для тестирования
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func TestHealthHandler(t *testing.T) {
// Создаём фейковый HTTP-запрос
req := httptest.NewRequest(http.MethodGet, "/health", nil)
req.Header.Set("X-Request-ID", "test-123")
// Создаём фейковый ResponseWriter (захватывает ответ)
rec := httptest.NewRecorder()
// Вызываем хендлер
healthHandler(rec, req)
// Проверяем ответ
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
var body map[string]string
err := json.NewDecoder(rec.Body).Decode(&body)
require.NoError(t, err)
assert.Equal(t, "ok", body["status"])
}
# Запуск всех тестов
go test ./...
# Запуск тестов с детальным выводом
go test -v ./...
# Запуск тестов конкретного пакета
go test -v ./internal/service/
# Запуск конкретного теста
go test -v -run TestIsValidEmail ./internal/service/
# Запуск конкретного подтеста
go test -v -run TestUserService_Register_Success ./internal/service/
# Покрытие кода
go test -cover ./...
# Покрытие с детальным профилем
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # Открыть в браузере
# Покрытие по функциям
go tool cover -func=coverage.out
# Гонка данных (ОБЯЗАТЕЛЬНО!)
go test -race ./...
| Функция | Поведение при ошибке | Когда использовать |
|---|---|---|
assert.Equal(t, a, b) | Тест ПРОДОЛЖАЕТСЯ | Несколько независимых проверок |
require.Equal(t, a, b) | Тест ПРЕРЫВАЕТСЯ | Без этого результата дальнейшие проверки бессмысленны |
assert.NoError(t, err) | Продолжается | Ошибка не критична для следующих проверок |
require.NoError(t, err) | Прерывается | Если err != nil, дальнейшие проверки приведут к панике |
| Библиотека | Аналог в JS | Назначение |
|---|---|---|
testing (std) | jest runner | Запуск тестов, бенчмарки |
testify/assert | expect().toBe() | Проверки (assertions) |
testify/require | assert + остановка | Жёсткие проверки |
gomock | jest.mock() | Моки интерфейсов |
httptest (std) | supertest | Тестирование HTTP |
testcontainers-go | testcontainers | Интеграционные тесты (урок 22) |
💡 Best practices от сеньоров:
💡 Для Node.js разработчика: