Тестирование — важнейший аспект разработки программного обеспечения, особенно для веб‑приложений. В Go тестирование встроено в язык и предоставляет мощные инструменты для написания и выполнения тестов. В этой статье мы рассмотрим поток веб‑приложения на Go, как писать модульные тесты для каждого слоя приложения.

Поток веб-приложения Go

При разработке приложений на Go важно понимать, как работает поток данных и управления через каждый слой приложения. Типичный поток может выглядеть следующим образом:

На этой схеме запрос начинается в браузере и проходит через промежуточный слой, который затем передает его контроллеру. Контроллер взаимодействует с менеджером, который затем передает данные сервисному слою. Сервисный слой, в свою очередь, взаимодействует со слоем валидации для проверки входных данных и бизнес‑правил, а также с хранилищем для сохранения данных, утилитами и вспомогательными модулями для дополнительной функциональности. Наконец, хранилище взаимодействует с моделью, которая и обрабатывает полученный запрос.

Написание модульных тестов

Чтобы обеспечить качество и корректность работы каждого слоя нашего приложения, мы должны написать как модульные, так и интеграционные тесты. Юнит‑тесты должны быть направлены на тестирование отдельных единиц кода в изолированном режиме, например, функций или методов в рамках одного модуля. Написание модульных тестов для каждого слоя приложения поможет нам выявить проблемы на ранних этапах разработки и убедиться, что каждый юнит кода ведет себя правильно.

Например, чтобы написать модульные тесты для сервисного слоя, мы можем создать макетные реализации для слоя проверки, репозитория, утилит и вспомогательных модулей, а затем протестировать каждый метод сервисного слоя в изоляции. С помощью макетов мы можем управлять вводом и выводом каждого метода и убедиться, что сервисный слой ведет себя так, как ожидается.

Для написания модульных тестов мы можем использовать фреймворк тестирования, например встроенный в Go пакет тестирования или сторонний пакет, например testify. Эти фреймворки предоставляют инструменты для создания имитационных реализаций зависимостей, запуска тестов и генерации отчетов о покрытии.

Пример теста

Допустим, у нас есть сервисный слой, который обрабатывает аутентификацию пользователей, и у него есть следующий метод:

type AuthService interface {
    Authenticate(username string, password string) (bool, error)
}

type AuthServiceImpl struct {
    validator ValidationService
    repo      UserRepository
}

func NewAuthServiceImpl(validator ValidationService, repo UserRepository) AuthService {
    return &AuthServiceImpl{
        validator: validator,
        repo:      repo,
    }
}

func (s *AuthServiceImpl) Authenticate(username string, password string) (bool, error) {
    if err := s.validator.ValidateUsername(username); err != nil {
        return false, err
    }
    if err := s.validator.ValidatePassword(password); err != nil {
        return false, err
    }
    return s.repo.Authenticate(username, password)
}

//Validation Service 
type ValidationService interface {
 ValidateUsername(username string) error
 ValidatePassword(password string) error
}


type ValidationServiceImpl struct{}

func (svc *ValidationServiceImpl) ValidateUsername(username string) error {
 // perform validation logic
 return nil // return nil if validation succeeds, or an error if it fails
}

func (svc *ValidationServiceImpl) ValidatePassword(password string) error {
 // perform validation logic
 return nil // return nil if validation succeeds, or an error if it fails

}

type UserRepository interface {
 Authenticate(username string, password string) (bool, error)
}

type UserRepositoryImpl struct{}

func (repo *UserRepositoryImpl) Authenticate(username string, password string) (bool, error) {
 // perform authentication logic
 return true, nil
}

type AuthService interface {
    Authenticate(username string, password string) (bool, error)
}

type AuthServiceImpl struct {
    validator ValidationService
    repo      UserRepository
}

func NewAuthServiceImpl(validator ValidationService, repo UserRepository) AuthService {
    return &AuthServiceImpl{
        validator: validator,
        repo:      repo,
    }
}


func (s *AuthServiceImpl) Authenticate(username string, password string) (bool, error) {
    if err := s.validator.ValidateUsername(username); err != nil {
        return false, err
    }

    if err := s.validator.ValidatePassword(password); err != nil {
        return false, err
    }
    return s.repo.Authenticate(username, password)
}

//Validation Service 

type ValidationService interface {
 ValidateUsername(username string) error
 ValidatePassword(password string) error
}

type ValidationServiceImpl struct{}

func (svc *ValidationServiceImpl) ValidateUsername(username string) error {
 // perform validation logic
 return nil // return nil if validation succeeds, or an error if it fails
}

func (svc *ValidationServiceImpl) ValidatePassword(password string) error {
 // perform validation logic
 return nil // return nil if validation succeeds, or an error if it fails

}

type UserRepository interface {
 Authenticate(username string, password string) (bool, error)
}

type UserRepositoryImpl struct{}

func (repo *UserRepositoryImpl) Authenticate(username string, password string) (bool, error) {
 // perform authentication logic
 return true, nil

}

Чтобы протестировать этот сервисный слой, мы можем создать макеты реализаций зависимостей ValidationService и UserRepository, например, с помощью библиотеки testity/mock.

type MockValidationService struct {
 mock.Mock
}

func (m *MockValidationService) ValidateUsername(username string) error {
 args := m.Called(username)
 return args.Error(0)
}

func (m *MockValidationService) ValidatePassword(password string) error {
 args := m.Called(password)
 return args.Error(0)
}

type MockUserRepository struct {
 mock.Mock
}

 func (m *MockUserRepository) Authenticate(username string, password string) (bool, error) {

 args := m.Called(username, password)
 return args.Bool(0), args.Error(1)
}

 

func TestAuthenticate(t *testing.T) {
 username := "testuser"
 password := "testpassword"

 mockValidator := new(MockValidationService)
 mockValidator.On("ValidateUsername", username).Return(nil)
 mockValidator.On("ValidatePassword", password).Return(nil)
 mockRepo := new(MockUserRepository)
 mockRepo.On("Authenticate", username, password).Return(true, nil)
 authService := NewAuthServiceImpl(mockValidator, mockRepo)
 authenticated, err := authService.Authenticate(username, password)

 if err != nil {
  t.Errorf("Unexpected error: %v", err)
 }

 if !authenticated {
  t.Error("Expected authentication to succeed, but it failed")
 }
}

В этом примере мы импортируем пакет mock из набора инструментов testify и создаем имитационные реализации зависимостей ValidationService и UserRepository с помощью структур MockValidationService и MockUserRepository. Затем мы используем метод On, чтобы указать ожидаемое поведение каждого вызова метода, и передаем макеты зависимостей в метод NewAuthServiceImpl для создания нового экземпляра AuthService.

Наконец, мы вызываем метод Authenticate с тестовыми данными и проверяем, что результат соответствует ожиданиям. Пакет mock позаботится о создании необходимых реализаций и проверке того, что они вызываются так, как ожидалось.

Заключение

Написание модульных тестов это важная часть разработки качественного веб‑приложения на Go. Тестируя каждый слой приложения в отдельности и как систему, мы можем выявить проблемы на ранних этапах разработки и убедиться, что приложение работает правильно.


Если вы работаете с Go и интересуетесь вопросами тестирования, приглашаем вас на открытые занятия курса «Автоматизированное тестирование веб‑сервисов на Go».

Также можно пройти вступительное тестирование, чтобы узнать, достаточно ли ваших текущих знаний для поступления на курс автоматизации тестирования на Go.

Комментарии (2)


  1. Denius
    24.07.2025 17:29

    Эх, жаль статья чуть припозднилась. Недавно как раз проходил этот путь, только начав писать тесты к уже работающему коду. И как оказалось, толком найти ничего в инете на эту тему нет. Либо все какое-то жутко сложное, либо слишком примитивное. В итоге набив шишек сам пришел к описанному в статье. Но потратил кучу времени понимая и разбираясь. Сейчас имею практически то, что расписано. Всяческие GoMock и прочие генераторы обошел стороной, ну не привык я к пользованию ими, мне нужно понимание, что делается, как и почему. Поэтому testify прям зашёл, тем более что уже использовал для более простых тестов, без мокирования.

    Чего не хватает: хорошего примера, как на таком стеке замокать БД. В принципе у меня уже понимание есть, и туда руки дойдут.

    А за статью, ее простоту и понятность хотел бы поставить палец вверх, но по какой-то исторической особенности Хабры это дано только особо активным пользователям. Ну а я читатель


  1. MyraJKee
    24.07.2025 17:29

    Чето как-то... Почему именно testify? А не gomock например