Ответ
В Go сообществе и в индустрии бэкенд-разработки принято писать несколько типов тестов для обеспечения качества и надежности кода:
-
Юнит-тесты (Unit Tests):
- Назначение: Тестирование отдельных, наименьших изолированных частей кода (функций, методов) без внешних зависимостей.
- Инструменты: Стандартный пакет
testing
. Для изоляции часто используются моки (например, сgomock
) или стабы. -
Пример:
func Add(a, b int) int { return a + b } func TestAdd(t *testing.T) { got := Add(2, 3) want := 5 if got != want { t.Errorf("Add(2, 3) = %d; want %d", got, want) } }
-
Интеграционные тесты (Integration Tests):
- Назначение: Проверка взаимодействия между несколькими компонентами системы (например, сервис с базой данных, с внешним API, с очередью сообщений).
- Инструменты: Могут использовать реальные или тестовые экземпляры зависимостей (например,
testcontainers
для запуска БД в Docker). -
Пример (тестирование сервиса с БД):
package main import ( "database/sql" "testing" _ "github.com/mattn/go-sqlite3" // Пример драйвера для SQLite ) // Предположим, у нас есть UserService, который работает с БД type User struct { ID int Email string } type UserService struct { db *sql.DB // Или другой интерфейс для работы с БД } func NewUserService(db *sql.DB) *UserService { return &UserService{db: db} } func (s *UserService) CreateUser(email string) (User, error) { // Логика создания пользователя в БД res, err := s.db.Exec("INSERT INTO users (email) VALUES (?)", email) if err != nil { return User{}, err } id, _ := res.LastInsertId() return User{ID: int(id), Email: email}, nil } func TestUserService_CreateUser(t *testing.T) { // Инициализация тестовой БД (например, in-memory SQLite) db, err := sql.Open("sqlite3", ":memory:") // Пример для SQLite if err != nil { t.Fatalf("Failed to open DB: %v", err) } defer db.Close() // Создание таблицы для теста _, err = db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT)") if err != nil { t.Fatalf("Failed to create table: %v", err) } service := NewUserService(db) user, err := service.CreateUser("test@example.com") if err != nil { t.Errorf("Failed to create user: %v", err) } if user.Email != "test@example.com" { t.Errorf("Expected email 'test@example.com', got '%s'", user.Email) } if user.ID == 0 { t.Errorf("Expected non-zero user ID, got %d", user.ID) } // Дополнительные проверки, например, что пользователь действительно в БД var retrievedEmail string err = db.QueryRow("SELECT email FROM users WHERE id = ?", user.ID).Scan(&retrievedEmail) if err != nil { t.Errorf("Failed to retrieve user from DB: %v", err) } if retrievedEmail != "test@example.com" { t.Errorf("Expected retrieved email 'test@example.com', got '%s'", retrievedEmail) } }
-
E2E-тесты (End-to-End Tests):
- Назначение: Тестирование всей системы целиком, имитируя реальные пользовательские сценарии через публичные интерфейсы (например, HTTP API, UI).
- Инструменты: Могут использовать
httptest
для API, или внешние инструменты для UI-тестирования.
Дополнительные важные типы тестов и практики в Go:
- Табличные тесты (Table-Driven Tests): Очень распространены в Go для тестирования функций с множеством входных данных и ожидаемых результатов, используя
t.Run
для подтестов. - Мокирование интерфейсов: Использование библиотек вроде
gomock
для создания моков интерфейсов, что позволяет изолировать юнит-тесты от конкретных реализаций. - Бенчмарки (Benchmarks): Функции, начинающиеся с
Benchmark
, измеряют производительность кода. - Примеры (Examples): Функции, начинающиеся с
Example
, демонстрируют использование кода и могут быть проверены компилятором.
Характеристики хороших тестов:
- Быстрые: Особенно юнит-тесты, чтобы их можно было запускать часто.
- Изолированные: Каждый тест должен быть независим от других.
- Детерминированные: Всегда дают один и тот же результат при одних и тех же входных данных.
- Читаемые: Легко понять, что тестируется и почему.
- Надежные (Reliable): Не "флапают" (не дают случайных сбоев).