Раздел 24 · Микросервисы (Go)

Неделя 2: чистая архитектура, слои, тесты, моки

middle~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Неделя 2: чистая архитектура, слои, тесты, моки

На первой неделе main.go мог быть толстым, потому что фокус был на контрактах. Теперь резка по слоям и нормальное тестовое покрытие. Без этого сервис не масштабируется по людям: новый разработчик не понимает где править логику, где доступ к БД, где транспорт.

Цели недели

  1. Привести каждый сервис к чистой архитектуре с явными слоями.
  2. Сгенерировать моки для интерфейсов через mockery.
  3. Покрыть unit-тестами service-слой минимум на 40%.
  4. Настроить вывод coverage в README через CI и Gist.
  5. Поднять reusable GitHub Actions workflows: lint, test, coverage.

Структура одного сервиса

order/
├── cmd/
│   └── main.go                      # точка входа, сборка зависимостей
├── go.mod
└── internal/
    ├── api/                          # transport
    │   └── order/v1/
    │       ├── api.go                # struct реализующая интерфейс из shared/pkg/openapi
    │       ├── create.go
    │       ├── get.go
    │       ├── pay.go
    │       └── cancel.go
    ├── client/                       # вызовы наружу
    │   ├── converter/
    │   │   └── part.go               # proto -> domain
    │   └── grpc/
    │       ├── client.go             # интерфейсы клиентов
    │       ├── mocks/                # mockery output
    │       ├── inventory/v1/         # реализация для inventory.proto
    │       └── payment/v1/           # реализация для payment.proto
    ├── converter/
    │   └── order.go                  # domain <-> dto api
    ├── model/
    │   ├── order.go                  # domain types
    │   ├── part.go
    │   └── error.go                  # доменные ошибки
    ├── repository/                   # хранилище
    │   ├── repository.go             # интерфейс
    │   ├── converter/
    │   │   └── order.go              # domain <-> repo model
    │   ├── mocks/
    │   ├── model/                    # repo-модель (как лежит в БД)
    │   │   └── order.go
    │   └── order/                    # реализация (in-memory на этой неделе)
    │       ├── repository.go
    │       ├── create.go
    │       ├── get.go
    │       └── update.go
    └── service/                      # бизнес-логика
        ├── service.go                # интерфейс
        ├── mocks/
        └── order/
            ├── service.go
            ├── create.go
            ├── create_test.go
            ├── get.go
            ├── get_test.go
            ├── pay.go
            ├── pay_test.go
            ├── cancel.go
            ├── cancel_test.go
            └── suite_test.go         # общий setup для table-driven

Главные принципы:

  • api не знает про БД, только про domain-типы и интерфейс service,
  • service не знает про HTTP/gRPC, только про domain и интерфейсы repository/client,
  • repository не знает про domain в чистом виде, у него своя модель (как лежит в БД), и converter переводит,
  • client не знает про domain, у него своя модель (как пришло по proto), и converter переводит,
  • converter-ы это маленькие чистые функции, легко тестируются.

Зачем разные модели для api/repo/client

Соблазн, одна модель на все слои, чтобы не дублировать поля. Это быстрая дорога к боли:

  • модель в API растёт под фронт (added/removed поля по мере фичей),
  • модель в БД растёт под индексы и денормализацию,
  • модель в client-е растёт под чужой контракт,
  • если всё одно, любое изменение в одном месте ломает три других.

Поэтому: три модели плюс converter-ы. Стоимость, +20% кода. Выгода, изменения локализованы.

Mockery, генерация моков по интерфейсам

# .mockery.yaml
filename: 'mock_{{.InterfaceName | snakecase}}.go'
dir: '{{.InterfaceDir}}/mocks'
mockname: 'Mock{{.InterfaceName}}'
outpkg: mocks
all: true
packages:
  github.com/me/myproject/order/internal/repository:
    config:
      include-regex: ".*Repository"
  github.com/me/myproject/order/internal/service:
    config:
      include-regex: ".*Service"
  github.com/me/myproject/order/internal/client/grpc:
    config:
      include-regex: ".*Client"
mockery

Получаем готовые моки рядом с интерфейсами. В тестах:

mockRepo := mocks.NewMockOrderRepository(t)
mockRepo.EXPECT().Create(mock.Anything, mock.Anything).Return(nil)
mockRepo.EXPECT().Get(mock.Anything, "uuid-1").Return(testOrder, nil)

svc := order.NewService(mockRepo, mockInventory, mockPayment)

t.Cleanup в mock автоматически проверяет что все ожидания выполнены.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.