Тестирование — важнейший аспект разработки программного обеспечения, особенно для веб‑приложений. В 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».
31 июля в 20:00 — открытый урок «Псевдосервер за 15 минут: учим SoapUI делать вид, что он API». Разберём, как с помощью SoapUI эмулировать поведение веб‑сервиса без запуска реального бэкенда.
12 августа в 20:00 — открытый урок «MITM: почему бесплатный VPN знает о вас больше, чем мама». Поговорим о перехвате трафика, анализе данных и связанных с этим рисках.
Также можно пройти вступительное тестирование, чтобы узнать, достаточно ли ваших текущих знаний для поступления на курс автоматизации тестирования на Go.