Какие тесты принято писать

Ответ

В Go сообществе и в индустрии бэкенд-разработки принято писать несколько типов тестов для обеспечения качества и надежности кода:

  1. Юнит-тесты (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)
          }
      }
  2. Интеграционные тесты (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)
          }
      }
  3. E2E-тесты (End-to-End Tests):

    • Назначение: Тестирование всей системы целиком, имитируя реальные пользовательские сценарии через публичные интерфейсы (например, HTTP API, UI).
    • Инструменты: Могут использовать httptest для API, или внешние инструменты для UI-тестирования.

Дополнительные важные типы тестов и практики в Go:

  • Табличные тесты (Table-Driven Tests): Очень распространены в Go для тестирования функций с множеством входных данных и ожидаемых результатов, используя t.Run для подтестов.
  • Мокирование интерфейсов: Использование библиотек вроде gomock для создания моков интерфейсов, что позволяет изолировать юнит-тесты от конкретных реализаций.
  • Бенчмарки (Benchmarks): Функции, начинающиеся с Benchmark, измеряют производительность кода.
  • Примеры (Examples): Функции, начинающиеся с Example, демонстрируют использование кода и могут быть проверены компилятором.

Характеристики хороших тестов:

  • Быстрые: Особенно юнит-тесты, чтобы их можно было запускать часто.
  • Изолированные: Каждый тест должен быть независим от других.
  • Детерминированные: Всегда дают один и тот же результат при одних и тех же входных данных.
  • Читаемые: Легко понять, что тестируется и почему.
  • Надежные (Reliable): Не "флапают" (не дают случайных сбоев).