Рекомендации к написанию автотестов
Перед началом следует ознакомиться с Autotests Guide. Это руководство дополняет его техническими деталями для Go/Goravel: где лежат тесты и фабрики, как устроены компонентные тесты HTTP API, search-эндпоинтов и Kafka-handlers.
Выбрать уровень проверки
Точка входа теста определяет, какое утверждение он способен подтвердить.
| Проверяемое поведение | Подход |
|---|---|
| Чистое преобразование, registry или алгоритм | Unit-тест пакета без компонентной БД |
| HTTP route, binding, validation, mapping | Настоящий запрос через ComponentSuite.Http и нужные dependency doubles; низкоуровневый wrapper — TestCase.Http |
| Изменение SQL, action, command или handler | ComponentSuite и пакетный RunComponentTests |
| Elasticsearch-сценарий Catalog Cache | Его ElasticComponentSuite и изолированные индексы |
| Доставка брокером или живая upstream-интеграция | Отдельная интеграционная проверка в подготовленном окружении |
Прямой тест action не проверяет route и HTTP-validation. Вызов Kafka Handle не проверяет consumer group и commit offset. Fake подтверждает поведение приложения на заданных ответах, а не совместимость с живым поставщиком.
Структура сервиса с точки зрения тестов
Компонентные тесты размещаются рядом с модулем, который они проверяют. Корневая директория tests/ содержит только общую инфраструктуру, которую переиспользуют все модули.
.
├── app
│ ├── adapters
│ │ └── db
│ │ ├── offer.go
│ │ └── offer_factory.go
│ └── modules
│ └── offers
│ ├── factories
│ ├── handlers
│ └── tests
│ ├── offers
│ │ ├── main_test.go
│ │ ├── search_test.go
│ │ ├── search_filter_test.go
│ │ └── search_include_test.go
│ └── kafka
│ ├── main_test.go
│ └── handler_test.go
└── tests
├── test_case.go
├── spec_request.go
├── spec_validate.go
├── component.go
├── component_suite.go
└── search_suite.go
Внутри app/modules/{module}/tests/ обычно создают подпапку по ресурсу или хэндлеру (offers, brands, auth). Один файл тестов соответствует одному методу API или одной группе близких сценариев: login_test.go, search_test.go, patch_test.go.
Файлы, которые генерирует OpenAPI (ensi-gog server), создаются один раз. Повторный запуск генератора не перезаписывает уже существующие тесты — их дополняют вручную.
Корневой пакет tests/
Пакет goravel/tests (директория tests/ в корне сервиса) подключается в тестах модуля как apptests "goravel/tests".
Основные элементы:
TestCase— низкоуровневая обёртка: методHttpдобавляет проверку запроса и ответа по OpenAPI (черезSpecRequestи kin-openapi). Сам по себеTestCaseне изолирует Kafka fake между тестами и не проверяет сообщения в teardown.ComponentSuite— содержитsuite.SuiteиTestCase, перед каждым тестом устанавливает новый Kafka fake и в teardown отклоняет непроверенные публикации. В SQL-сервисах также очищает таблицы и предоставляет ORM-хелперыCreateиCreateQuietly.RunComponentTests— вызывается изTestMainпакета тестов, когда нужна изолированная БД на весь пакет (см. ниже).SearchSuite[T]— обёртка для типизированных:search-запросов: POST с валидацией по спеке и разбор ответа вSearchResponse[T].
При старте тестов приложение поднимается через bootstrap.Boot() (см. tests/test_case.go).
TestMain и настройка базы данных
Если тестам нужна PostgreSQL (подготовка данных в таблицах, проверка side effects после HTTP или Kafka), в том Go-пакете, где лежат такие тесты, добавляют файл main_test.go с функцией TestMain:
package tests
import (
"os"
"testing"
apptests "goravel/tests"
)
func TestMain(m *testing.M) {
os.Exit(apptests.RunComponentTests(m))
}
RunComponentTests выполняет одноразовую настройку для всего пакета:
- создаёт отдельную временную базу данных;
- переключает конфигурацию приложения на неё;
- прогоняет миграции;
- запускает тесты пакета;
- по завершении удаляет временную БД.
Между отдельными тестами внутри пакета ComponentSuite.SetupTest очищает прикладные таблицы, чтобы тесты не влияли друг на друга. Тесты одного пакета с общей БД должны выполняться последовательно (не параллелить внутри пакета без отдельной изоляции).
В Customer API Gateway HTTP-тесты встраивают apptests.ComponentSuite без main_test.go и RunComponentTests. В SQL-варианте ComponentSuite runner нужен даже для сценария, который сам не читает БД: его требует базовый setup. Для отдельной HTTP/OpenAPI-проверки без компонентного lifecycle допустим TestCase, но автоматической проверки исходящих Kafka-сообщений у такого теста не будет.
Kafka-handlers, которые пишут в БД, обычно тестируют в отдельном подпакете tests/kafka/ со своим main_test.go, по той же схеме.
Изоляция PostgreSQL и окружение
Текущий runner подключается к служебной БД postgres, создаёт временную БД с уникальным именем, меняет connection config и обновляет приложение. После переключения выполняются миграции и подмена Kafka binding. По завершении закрываются подключения приложения и удаляется только созданная этим процессом БД. Обёртка os.Exit(apptests.RunComponentTests(m)) позволяет cleanup runner-а закончиться до завершения процесса.
Для этого пользователь .env.testing должен иметь доступ к служебной БД, права создания/удаления временных БД и необходимые права для миграций. Проверяйте выбранные host, connection и schema до работы с фикстурами. Если пакет использует дополнительные connections, их изоляцию нужно обеспечить отдельно: смена одной postgres connection не перенаправляет произвольный второй клиент автоматически.
Миграции выполняются один раз на пакет. Текущая очистка строит список таблиц выбранной schema и выполняет TRUNCATE ... RESTART IDENTITY CASCADE перед сценарием. Новая таблица в другой schema или созданная после построения списка требует проверки механизма очистки. Не заменяйте его миграцией refresh или общей rollback-транзакцией: action может открывать собственную транзакцию или соединение.
RESTART IDENTITY не сбрасывает счётчики и случайные значения фабрик в Go. Используйте сохранённые ID, а не предположение, что следующая запись имеет ID 1. Создавайте только нужные связи и различайте локальный PK и внешний ID — см. БД.
Тесты разделяют конфигурацию и singleton bindings процесса даже при отдельной БД на пакет. Поэтому внутри такого пакета не используйте t.Parallel(). Для .env.testing проверьте отсутствие одноимённых локальных override-файлов ближе к пакету, если версия framework ищет env вверх по каталогам. Флаги отключения фоновых consumer/worker не отключают эффекты, вызываемые action синхронно.
Базовый suite для HTTP-компонентных тестов
Тест API оформляют через testify/suite, встраивая ComponentSuite. Например, в Customer API Gateway:
type LoginTestSuite struct {
apptests.ComponentSuite
}
func TestLoginTestSuite(t *testing.T) {
suite.Run(t, new(LoginTestSuite))
}
В SQL-сервисах используется такое же встраивание, но пакету дополнительно нужен TestMain с RunComponentTests. ComponentSuite уже содержит suite.Suite и TestCase; повторно встраивать их не нужно.
После каждого теста с фейковыми HTTP-клиентами сбрасывают registry, сохраняя общий Kafka teardown:
func (s *LoginTestSuite) TearDownTest() {
s.ComponentSuite.TearDownTest()
ensihttpfake.Reset(facades.App().MakeHttp())
}
Если тест использует кэш или другие глобальные singletons, их тоже очищают. Сброс HTTP fake не заменяет проверку Kafka.
Setup, teardown и ручные t.Run
suite.Run вызывает suite hooks. При переопределении SetupTest и TearDownTest явно вызывайте соответствующий метод встроенного suite. Иначе setup не установит новый fake, а teardown пропустит проверку сообщений:
func (s *LoginTestSuite) SetupTest() {
s.ComponentSuite.SetupTest()
// Дополнительная подготовка сценария.
}
Обычный t.Run не вызывает методы suite автоматически. В ручном тесте или конструкторе search-suite соблюдайте последовательность:
s := &apptests.ComponentSuite{}
s.SetT(t)
s.SetupTest()
t.Cleanup(s.TearDownTest)
Регистрация teardown после setup обязательна: t.Cleanup работает в обратном порядке. Проверка сообщений должна выполниться до сброса Kafka fake, зарегистрированного setup-ом. Не регистрируйте второй teardown, если его уже вызывает suite.Run.
Вложенный s.Run также не повторяет SetupTest/TearDownTest: testify использует отдельные hooks SetupSubTest/TearDownSubTest. Если внутри callback вы вручную вызвали s.SetupTest(), завершайте эту фазу через defer s.TearDownTest() в том же callback, до восстановления родительского s.T().
Вызов эндпоинта и проверка по OpenAPI
Вызов тестируемого эндпоинта делается через метод Http текущего suite. Метод принимает *testing.T (или s.T() внутри suite) и возвращает обёртку над HTTP-клиентом Goravel с валидацией по спецификации из resources/openapi/.
Примеры:
resp, err := s.Http(s.T()).Get("/api/v1/catalog/brands/10")
resp, err := s.Http(s.T()).Post(
"/api/v1/auth/login",
factories.LoginRequest().WithLogin(login).WithPassword(password).MustMakeReader(),
)
resp, err := s.Http(s.T()).PostJSON("/api/v1/offers/offers:search", map[string]any{
"sort": []string{"-id"},
})
resp, err := s.Http(s.T()).PatchJSON(
fmt.Sprintf("/api/v1/elastic/indexer-timestamps/%d", id),
dto.PatchIndexerTimestampRequest{LastSchedule: &updated},
)
После вызова проверяют, что ошибки транспорта нет (s.Require().NoError(err)), затем статус и тело:
resp.AssertStatus(200)
resp.AssertOk()
resp.AssertJson(map[string]interface{}{
"data": map[string]interface{}{
"access_token": accessToken,
},
})
Для разбора ответа в структуру используют resp.Bind(&result).
По умолчанию и запрос, и ответ сверяются с OpenAPI. Если нужно намеренно передать невалидное тело (для проверки 400 ValidationError), перед POST включают отключение проверки запроса:
resp, err := s.Http(s.T()).
SkipRequestValidation().
Post("/api/v1/auth/login", strings.NewReader(`{}`))
Генератор OpenAPI создаёт заготовки TestStatus_{code} для кодов ответа из спеки в диапазоне 1xx–4xx (5xx обычно не генерируются). Эти тесты — минимальный каркас. Их нужно дополнять кейсами на реальную бизнес-логику: маппинг полей, filter/sort/include, пустые результаты, права доступа, проксирование ошибок апстрима, обогащение ответа несколькими клиентами и т.д.
Подмена внешних HTTP-клиентов
В компонентных тестах сервис не должен ходить в другие микросервисы по сети. Для этого используются сгенерированные клиенты Ensi (*-client-go) и пакет goravel-ensi-http-client/fake.
В стандартном Goravel HTTP-тестировании обычно один вызов Http().Fake(map[string]any{...}), который задаёт карту URL → ответ. В Ensi-клиентах fake устроен иначе: пакет каждого клиента экспортирует Fake(), который возвращает fluent-объект для конкретных методов API (SearchOneBrands, CreateToken, SearchOrders и т.д.). Это позволяет:
- регистрировать несколько stub-ов подряд (несколько методов одного клиента или несколько клиентов в одном тесте);
- собирать ответы через типизированные response-фабрики;
- проверять исходящие запросы через
Assert....
Stub-ы накапливаются в registry и применяются к HTTP factory приложения только после Bind(). Если описали ответ, но забыли вызвать Bind(), тест пойдёт в сеть или упадёт на «stray request» — stub просто не подключится.
Пример мока ответа внешнего сервиса:
func (s *GetTestSuite) TestStatus_200() {
fake := pimclient.Fake()
fake.SearchOneBrands(
pimclient.NewBrandResponseFactory().
WithData(
pimclient.NewBrandFactory().
WithId(10).
WithCode("sony").
WithName("Sony"),
),
).Bind() // обязательно: без Bind stub не активен
resp, err := s.Http(s.T()).Get("/api/v1/catalog/brands/10")
s.Require().NoError(err)
resp.AssertStatus(200)
resp.AssertJson(map[string]interface{}{
"data": map[string]interface{}{
"id": float64(10),
"name": "Sony",
"code": "sony",
},
})
s.Require().True(fake.AssertSearchOneBrands(func(request client.Request) bool {
var body map[string]any
if json.Unmarshal([]byte(request.Body()), &body) != nil {
return false
}
filter, _ := body["filter"].(map[string]any)
return filter["id"] == float64(10)
}))
}
В одном тесте часто нужно несколько апстримов. Для каждого клиента можно вызвать Fake() столько раз, сколько нужно, описать методы и в конце вызвать Bind() (отдельно на каждый клиент или после цепочки — как удобнее в конкретном API fake-объекта):
omsFake := omsclient.Fake()
omsFake.SearchOrders(/* response factory or map */).Bind()
catalogcacheclient.Fake().SearchElasticOffers(/* ... */).Bind()
crmclient.Fake().SearchCustomerFavorite(/* ... */).Bind()
После каждого теста с фейками вызывают ensihttpfake.Reset(facades.App().MakeHttp()) в TearDownTest, чтобы registry не перешёл в следующий кейс.
Assert... generated-клиента возвращает bool: его нужно передать в assert/require. В текущем runtime он подтверждает хотя бы один совпадающий вызов; это не проверка точного count или порядка. Для этих требований используйте доступный recorder либо строгие expectations текущей версии fake. Проверяйте method/target, path/query/body и значимые headers, а в rejected-ветках — отсутствие ненужных вызовов.
Не подменяйте собственный handler или action, если проверяется его поведение. Подменяйте внешнюю границу: generated HTTP client, Kafka producer или storage. Fake HTTP может обходить transport: timeout, initial-event middleware и circuit breaker требуют отдельной контролируемой проверки transport/server. Подробности — в HTTP-клиентах.
Подготовка данных в PostgreSQL
Когда тест проверяет чтение или изменение локальной БД, suite встраивает ComponentSuite, а пакет — TestMain с RunComponentTests.
Для подготовки фикстур используйте CreateQuietly: helper вызывает facades.Orm().Factory().CreateQuietly(...) и сохраняет модель без model events и observers:
var defaultOffer db.Offer
s.CreateQuietly(&defaultOffer)
var offer db.Offer
s.CreateQuietly(&offer, map[string]any{
"ProductID": product.ProductID,
"IsActive": false,
})
CreateQuietly сохраняет ранее накопленные Kafka-сообщения; это не Create со сбросом fake. Он отключает все model events, а не только Kafka-публикацию. Если проверяется сам observer или его работа нужна сценарию, используйте обычный s.Create(...) и проверьте созданные им эффекты.
Фабрики моделей лежат рядом с моделью в app/adapters/db/*_factory.go. У каждой есть метод Definition() с набором полей по умолчанию и опциональные With* для переопределения отдельных атрибутов в тесте.
После HTTP-запроса часто дополнительно читают строку из БД и сравнивают с ожиданием:
var stored db.IndexerTimestamp
s.Require().NoError(facades.Orm().Query().FindOrFail(&stored, timestamp.ID))
s.Equal(updated.Truncate(time.Microsecond), stored.LastSchedule.StdTime().UTC())
Данные, которые доказывают изменение
Задавайте значения под проверяемое поведение явно. Для true→false создайте true, для очистки nullable — заполненное поле, для сортировки — разные известные значения. Случайные factory defaults не должны определять, проходит ли тест.
Создайте постороннюю запись и сравните её состояние после операции, если проверяется ограничение владельцем/родителем. Для связи задайте реальный ключ, на который указывает references; не создавайте случайный FK в надежде, что нужная сущность существует.
Для timestamps используйте известное более раннее значение и новое чтение, учитывая точность PostgreSQL и timezone. time.Sleep делает проверку медленной и нестабильной. Если нужна проверка текущего времени, используйте предусмотренный сценарием clock либо разумный интервал вокруг операции.
HTTP-ответ из объекта в памяти не доказывает сохранение: после mutation загрузите новую модель. Для ошибки после части SQL-работы проверьте все затронутые таблицы; отсутствие основной строки не доказывает отсутствие осиротевшей связи.
Search-эндпоинты (:search)
Для POST :search удобно использовать SearchSuite[T]: в suite задают SearchPath, метод Search(payload) отправляет JSON, проверяет OpenAPI и возвращает типизированный SearchResponse[T].
type offerSearchSuite = apptests.SearchSuite[dto.Offer]
func newOffersSearch(t *testing.T) *offerSearchSuite {
s := &offerSearchSuite{SearchPath: "/api/v1/offers/offers:search"}
s.SetT(t)
s.SetupTest()
t.Cleanup(s.TearDownTest)
t.Cleanup(func() { ensihttpfake.Reset(facades.App().MakeHttp()) })
return s
}
В этом примере ensihttpfake — alias goravel-ensi-http-client/fake. Base setup регистрирует сброс Kafka fake, а t.Cleanup(s.TearDownTest) — обязательную проверку сообщений перед сбросом. Очистка HTTP registry сохраняется отдельно.
На один ресурс обычно выделяют несколько файлов:
search_test.go— базовый успешный сценарий, сортировка по умолчанию;search_filter_test.go— каждый публичный filter (в том числе границы дат);search_sort_test.go— каждое поле сортировки;search_include_test.go— каждый include и осмысленные комбинации.
Table-driven тесты
Для однотипных проверок (каждый filter, каждый sort, матрица «поле × значение») удобен table-driven стиль: таблица кейсов и один цикл for _, tc := range cases. Так проще добавлять новые поля без копирования boilerplate.
Пример для search filter в Catalog Cache (нужны TestMain и imports по пакету):
func TestOffersSearchFilters(t *testing.T) {
for _, tc := range []struct {
name string
attr string
attrValue any
otherValue any
filterKey string
filterVal any
}{
{"offer_id", "OfferID", uint64(123), uint64(124), "offer_id", uint64(123)},
{"is_active/true", "IsActive", true, false, "is_active", true},
{"is_active/false", "IsActive", false, true, "is_active", false},
} {
t.Run(tc.name, func(t *testing.T) {
s := newOffersSearch(t)
var offer db.Offer
s.CreateQuietly(&offer, map[string]any{tc.attr: tc.attrValue})
var other db.Offer
s.CreateQuietly(&other, map[string]any{tc.attr: tc.otherValue})
response := s.Search(map[string]any{
"filter": map[string]any{tc.filterKey: tc.filterVal},
})
s.Require().Len(response.Data, 1)
s.Equal(offer.ID, response.Data[0].Id)
})
}
}
Имя подтеста (t.Run(tc.name, ...)) помогает найти упавший filter. Вторая, неподходящая запись важна: без неё тест мог бы пройти даже при полностью неработающем фильтре.
Для sort проверяйте точный порядок ID и значения при совпадающих полях. Для include создавайте связанные и посторонние строки и проверяйте состав DTO с include и без него. Для pagination нужны несколько страниц, defaults, пустая страница, ошибочный cursor и отсутствие пропусков/дублей. Для date filters — значения ниже, на и выше границы и часовой пояс.
Полный pipeline и ограничения helpers — в SQL-поиске. Для search-тестов используйте test-search-endpoint, для аудита — review-search-contract из .agents/skills.
Фабрики
Тела запросов к своему API
Для POST/PATCH/PUT к своему API используют фабрики в app/modules/{module}/factories/ (генерируются из OpenAPI). Каркас можно править: Definition() и With*.
Фабрики клиентов и DTO
У сгенерированных клиентов и response/request DTO те же builder-ы на базе factory.Builder[T]. Для получения значения есть несколько методов — выбирайте по контексту:
| Метод | Когда использовать |
|---|---|
Make() | нужен (T, error) — обработка ошибки decode |
MustMake() | в тестах, когда ошибка сборки = падение теста |
MakeMap() / MustMakeMap() | нужен map[string]any с JSON-ключами |
MakeJSON() / MustMakeJSON() | JSON в виде []byte |
MakeReader() / MustMakeReader() | io.Reader для Post(..., reader) в HTTP-тесте |
Count(n) | n независимых экземпляров с одним набором With* |
Пример тела запроса в HTTP-тесте:
factories.LoginRequest().
WithLogin("user").
WithPassword("secret").
MustMakeReader()
Вложенные фабрики
Builder поддерживает вложенность: значение поля может быть другой фабрикой, а не готовым объектом. При финальном Make* вложенные builder-ы разрешаются автоматически через MakeAttribute().
Если сигнатура WithData, WithItems или другого метода поддерживает вложенный builder, можно передать фабрику без предварительного MustMake() / Make():
// верно: вложенная фабрика разрешится при сборке ответа
pimclient.NewBrandResponseFactory().
WithData(
pimclient.NewBrandFactory().
WithId(10).
WithName("Sony"),
)
// для слайса сущностей — элементы могут быть как MustMake(), так и фабрикой,
// если метод принимает []any и фабрики внутри map/response builder
omsclient.NewSearchOrdersResponseFactory().WithData([]any{
omsclient.NewOrderFactory().WithId(1).WithItems(&items),
})
Текущий builder принимает и готовые значения, и вложенные Maker, если сигнатура конкретного With* это допускает. Предпочитайте builder внутри builder-а для ленивой сборки, а Make* вызывайте на границе: body HTTP-запроса, локальная переменная или API, которое не разрешает вложенную фабрику. Проверяйте тип принимаемого поля; правило для any не означает, что каждый generated With* принимает произвольный builder.
Clone() builder-а копирует state поверхностно: вложенные maps, slices и builders могут оставаться общими. Не переиспользуйте изменяемую fixture между кейсами. MustMake* делает ошибку сборки фатальной, но само по себе не валидирует бизнес-правила API.
Kafka: handler, publisher и отказ
Входящий handler
Тест с ComponentSuite вызывает настоящий Handle с сериализованным ModelEvent[Payload], затем перечитывает SQL и проверяет связанные эффекты. Брокер для этого не нужен. Пример подготовки сообщения в тестах brands Catalog Cache:
payload := brandkafka.BrandPayload{ID: 101, Name: "Brand", Code: "brand"}
value, err := json.Marshal(appkafka.ModelEvent[brandkafka.BrandPayload]{
Event: appkafka.ModelEventCreate,
Attributes: payload,
})
s.Require().NoError(err)
s.Require().NoError(brandkafka.NewHandler().Handle(context.Background(), appkafka.Message{
TopicKey: brandkafka.TopicKey,
Value: value,
}))
var stored db.Brand
s.Require().NoError(facades.Orm().Query().Where("brand_id", payload.ID).FirstOrFail(&stored))
s.Equal(payload.Name, stored.Name)
s.Equal(payload.Code, stored.Code)
brandkafka — пакет app/modules/brands/kafka, appkafka — adapter, json и context — стандартные пакеты. В тесте propagation задайте известный context вместо Background. Прямой Handle не декодирует Kafka headers в context и не проверяет registry/подписку — это ответственность adapter/consumer.
Для handler-а выбирайте случаи по его политике: create/update/delete, relevant/irrelevant dirty, missing record, повтор без лишней записи, false/zero/null, malformed JSON, unsupported event и rollback. Полная реплика Catalog Cache и status-only consumer OMS имеют разные допустимые операции — см. Kafka.
Исходящие события
В ComponentSuite доступен s.Kafka, реализующий producer interface без брокера. Messages() сохраняет порядок и реальные сериализованные bytes через production encoder. Проверяйте topic key, key, envelope, attributes, dirty, headers и число сообщений.
Пример фрагмента проверки после операции с nameplate в CMS:
messages := s.AssertKafkaMessages(nameplatekafka.NameplateTopicKey)
s.Require().Len(messages, 1)
s.Equal(strconv.FormatUint(uint64(model.ID), 10), string(messages[0].Key))
var event appkafka.ModelEvent[nameplatekafka.NameplatePayload]
s.Require().NoError(json.Unmarshal(messages[0].Value, &event))
s.Equal(appkafka.ModelEventUpdate, event.Event)
s.Equal([]string{"is_active"}, event.Dirty)
s.False(event.Attributes.IsActive)
Здесь сценарий изменил только is_active с true на false у подготовленного model; прочие поля payload и headers проверяются по требованиям теста. nameplatekafka — пакет модуля CMS, strconv/json — стандартные пакеты. Проверка одного интересующего поля не заменяет проверку остальных значимых атрибутов.
В Go-шаблоне, CMS, OMS, Catalog Cache и Customer API Gateway ComponentSuite.TearDownTest завершает тест ошибкой, если fake содержит сообщения, а фаза не отмечена проверенной:
Kafka messages were produced but not asserted: topics [nameplates]
Чтение s.Kafka.Messages() и даже assertions над полученным списком сами по себе не меняют отметку suite. Для проверки используйте общие методы:
| Метод | Что делает |
|---|---|
s.AssertKafkaMessages("deliveries", "orders") | Сравнивает точное число и порядок topic keys, отмечает фазу проверенной и возвращает сообщения для assertions payload/key/headers |
s.AssertKafkaMessages() | Проверяет отсутствие сообщений |
s.ConfirmKafkaMessages() | Только отмечает фазу проверенной; вызывается после содержательных проверок через другой helper |
s.ResetKafka() | Очищает сообщения и сбрасывает отметку проверки перед следующей фазой |
Вызывайте assertions после завершения операции. Отметка общая для фазы и не отслеживает отдельно каждое сообщение, добавленное после проверки. Для следующей операции сначала проверьте накопленные события, затем вызовите ResetKafka(). Прямой s.Kafka.Reset() не сбрасывает отметку suite.
Фикстуры создавайте через CreateQuietly, когда их model events не входят в проверку. Это позволяет оставить во fake только события операции, не стирая ранее накопленные сообщения. Если подготовка намеренно использует Create, проверьте её события перед переходом к следующей фазе. Не очищайте события проверяемой операции ради прохождения teardown.
Ошибка публикации и replay
Для управляемого отказа используйте небольшой producer double с тем же интерфейсом: он возвращает ошибку на выбранном topic, а предыдущие сообщения делегирует s.Kafka. Подмените binding и его singleton cache через принятый helper, а восстановление зарегистрируйте через cleanup до вызова операции.
У OMS есть пример: delivery-order публикует deliveries, затем отправка orders завершается ошибкой. Тест должен подтвердить:
Handleвернул ошибку.- SQL доставки и заказа остался прежним после нового чтения.
- Первое сообщение
deliveriesсохранилось во fake; SQL rollback его не отменил. - После восстановления producer повтор того же входящего события завершается успешно и даёт ожидаемый список сообщений.
Не очищайте внешний префикс, прежде чем его проверить. Для отдельной фазы replay reset разрешён после assertions первой фазы. Затем проверьте результат в SQL и повторно отправленные события.
Для initial-event используйте известный initialevent.Event в context и проверьте header каждого сообщения. OMS AssertInitialEventHeaders в текущей версии ожидает ровно один header; если контракт требует дополнительные headers, нужна соответствующая проверка, а не слепое переиспользование этого ограничения.
Изменяющие операции и файлы
| Сценарий | Что подтвердить помимо HTTP-статуса |
|---|---|
| Create/update | Новые значения после чтения SQL, timestamps, связи и эффекты |
| PATCH | Missing сохраняет прежнее; заполненное nullable очищается; true→false и ненулевое→0 реально записаны |
| Forbidden/ownership | Посторонние строки прежние, внешних вызовов и удаления файлов нет |
| Batch delete | Проверен весь набор до разрушительных действий; поведение missing/duplicates соответствует контракту |
| No-op/repeat | Точная политика timestamps и повторной публикации |
| Ошибка зависимости | Нужный rollback/commit и уже выполненные внешние эффекты |
Для файлов отправляйте настоящий multipart и подменяйте storage на его границе. В CMS tests.MockStorage(t) восстанавливает binding через cleanup и проверяет ожидания; он рассчитан на один HTTP-запрос в последовательном тесте. Maintenance middleware проверяет framework/maintenance.json при каждом запросе, что нужно учитывать в expectations.
Mock доказывает вызовы с нужным disk/key. Для доказательства записи байтов используйте временный каталог с изолированным локальным driver. Exists() == true из mock этого не подтверждает. Проверяйте SQL metadata и чтение/удаление по тому же ключу, а для замены — состояние старого и нового объекта после каждого отказа.
Практические матрицы — в изменяющих операциях и файлах. Общий скилл — test-component-behavior; детали storage — implement-file-storage-operation.
Unit-тесты инфраструктуры
Логику адаптеров (app/adapters/kafka, queue, logging и т.д.) можно тестировать unit-тестами в том же Go-пакете (*_test.go рядом с кодом). Такие тесты не обязаны поднимать HTTP-сервер и OpenAPI; они проверяют чистые функции, конфигурацию, registry, формат сообщений.
Запуск тестов
Go-команды в проектах Ensi выполняют через elc, переменные окружения для тестов — из .env.testing:
elc go test ./... --env=.env.testing
elc go test ./app/modules/contents/tests/pages --env=.env.testing -count=1
elc go test ./app/modules/nameplates/tests/nameplates --env=.env.testing -count=1
Узкие примеры выше относятся к CMS; в OMS для Kafka используется ./app/modules/orders/tests/kafka. Для выбора одного suite/case сверяйте имена в пакете и используйте -run; -count=1 отключает повторное использование результата из test cache. Для изменений конкурентного кода есть elc mise run check:race.
Перед сдачей прогоняйте проверки затронутых пакетов и обязательные checks сервиса. Отчёт должен различать реально выполненный тест и добавленный пример; passing fake-тест не доказывает доставку Kafka или доступность внешнего API.
Исходные примеры
- CMS: runner PostgreSQL, очистка и suite.
- CMS: SpecRequest и response validation — фактические границы OpenAPI-проверок.
- Go-шаблон: ComponentSuite — общий Kafka lifecycle, assertions и
CreateQuietly. - Customer API Gateway: ComponentSuite — Kafka lifecycle без БД.
- OMS: ComponentSuite — Kafka teardown и assertions.
- OMS: delivery-order tests — частичная публикация, SQL rollback и replay.
- Catalog Cache: ElasticComponentSuite и изоляция индексов.
- Catalog Cache: brands tests — входящие model events и сохранённое состояние.