Опирается на правила:
GOTEST-1,GOTEST-2,GOTEST-3,GOTEST-X1,GOTEST-X2из Go Test Strategy → раздел 1. Базовые правила.
Важно знать
- Тест должен быть быстрым и детерминированным.
time.Sleep/assert.Eventually— признак недетерминированного дизайна, не способ «дать время».- Интеграционный тест =
httptest.NewServer(router)+ реальный PostgreSQL (Testcontainers) + HTTP-вызов черезhttp.DefaultClient.- Внешние HTTP-сервисы (платёж, логистика) — отдельный
httptest.NewServerс заглушкой; не трогаем реальные endpoints.- Kafka и Redis не поднимаем — build-тег
integrationили env-профиль их отключает; события проверяются через Outbox-таблицу.- Время и UUID — инжектируются через интерфейсы
Clock/IDGenerator; в тесте —fixedClock/seqIDGenerator.- Один тест — один сценарий, AAA-структура (
// Arrange,// Act,// Assert).require.*для критичных шагов (fatal on fail),assert.*для проверок в блоке Assert.
UCP-подход к тестам в Go строится вокруг той же идеи, что и в Java: тест быстрый (миллисекунды, не секунды), детерминированный (одинаковый результат при каждом прогоне), простой (один сценарий). Разница в инструментах: вместо Spring-контекста — httptest.NewServer, вместо @MockitoBean — интерфейсы-заглушки, вместо Awaitility — его полное отсутствие.
Что входит в интеграционный тест
GOTEST-1: полный стек внутри сервиса, минус внешние системы.
| Часть | Где в тесте |
|---|---|
| HTTP-сервер | httptest.NewServer(chi-роутер) |
| PostgreSQL | Testcontainers postgres.Run(ctx, "postgres:16-alpine") |
| HTTP-клиент | http.DefaultClient.Do(req) |
| Внешние HTTP | отдельный httptest.NewServer с http.HandlerFunc-заглушкой |
| Kafka | НЕ поднимаем; проверяем Outbox-таблицу через DatabasePreparer |
| Redis | НЕ поднимаем; подменяем на NoopCache build-тегом |
| Время | fixedClock — реализация интерфейса Clock |
| UUID | seqIDGenerator — реализация интерфейса IDGenerator |
Это даёт:
- End-to-end внутри сервиса — от HTTP-запроса до строки в PostgreSQL.
- Без асинхронных рассинхронизаций — нет ожидания Kafka consumer-а.
- Время прогона — один тест занимает 20–100 мс; инфраструктура поднимается один раз в
TestMain.
func TestCreateOrder_Success(t *testing.T) {
srv, prep := newTestServer(t)
// Arrange
prep.Clear(t).CreateCustomer(t, "c1")
// Act
resp, err := http.Post(srv.URL+"/orders", "application/json",
strings.NewReader(`{"customerId":"c1","amount":100}`))
require.NoError(t, err)
defer resp.Body.Close()
// Assert
require.Equal(t, http.StatusCreated, resp.StatusCode)
var body map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
assert.Equal(t, "c1", body["customerId"])
}
Все тесты детерминированные
GOTEST-2: никаких time.Sleep, polling-циклов, assert.Eventually.
// ПЛОХО — тест зависит от скорости CI-машины
time.Sleep(500 * time.Millisecond)
assert.NotNil(t, findOrder(t, pool, "o1"))
// ПЛОХО — Eventually скрывает недетерминированный дизайн
assert.Eventually(t, func() bool {
return findOrder(t, pool, "o1") != nil
}, 5*time.Second, 100*time.Millisecond)
Что плохо:
- Мигающий — на медленном CI падает, на быстром проходит; «починка» — увеличить timeout, снова мигает.
- Медленный — каждый
Sleep/Eventually— потеря сотен миллисекунд; сотня тестов = минуты. - Скрывает баги — асинхронная ошибка «событие запоздало на 80 мс» проходит в
Eventually(5s), но в production клиент уже получил timeout.
Корректно — синхронный поток:
Clockчерез интерфейс → в тестеfixedClockс заданным значением.IDGeneratorчерез интерфейс → в тестеseqIDGenerator(счётчик).- Outbox-relay вызывается явно:
relay.ProcessPending(ctx), не ждём фонового тика. - Kafka/Redis отключены — нет ожидания consumer-а.
type Clock interface {
Now() time.Time
}
type fixedClock struct{ at time.Time }
func (c *fixedClock) Now() time.Time { return c.at }
type IDGenerator interface {
Next() string
}
type seqIDGenerator struct{ n atomic.Int64 }
func (g *seqIDGenerator) Next() string {
return fmt.Sprintf("id-%d", g.n.Add(1))
}
GOTEST-X2: прямые вызовы time.Now() и uuid.New() в доменном коде (Handler/Service/Aggregate) запрещены — делают тест недетерминированным.
AAA-структура
GOTEST-3: один тест — один сценарий, три блока с пустой строкой между ними.
// TestCancelOrder_WhenAlreadyCancelled_Returns422 проверяет BR-ORDER-7:
// нельзя отменить заказ в статусе CANCELLED.
func TestCancelOrder_WhenAlreadyCancelled_Returns422(t *testing.T) {
srv, prep := newTestServer(t)
// Arrange
prep.Clear(t).
CreateCustomer(t, "c1").
CreateOrder(t, "o1", "c1", "CANCELLED")
// Act
req, err := http.NewRequest(http.MethodDelete, srv.URL+"/orders/o1", nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Assert
require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
var body map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
assert.Equal(t, "BR-ORDER-7", body["code"])
}
Каждый блок — одна ответственность:
- Arrange — очистка БД, создание фикстур, настройка заглушек времени/UUID.
- Act — один HTTP-вызов; не несколько, не «создать и сразу проверить».
- Assert — проверки ответа и побочных эффектов (строки в БД, Outbox-события).
Не «проверить весь жизненный цикл в одном тесте»
// ПЛОХО — мега-тест проверяет 5 переходов состояния
func TestOrderLifecycle(t *testing.T) {
// создать заказ
// подтвердить
// оплатить
// отгрузить
// отменить
}
Что плохо:
- Если падает шаг 3 — неизвестно, что не так с шагами 4–5.
- Имя теста не описывает ни одного конкретного сценария.
- Нельзя запустить один сценарий в изоляции.
Корректно — отдельные функции:
func TestCreateOrder_Success(t *testing.T) { ... }
func TestConfirmOrder_WhenDraft_Returns200(t *testing.T) { ... }
func TestPayOrder_WhenConfirmed_Returns200(t *testing.T) { ... }
func TestCancelOrder_WhenConfirmed_Returns200(t *testing.T) { ... }
Имена тестов
GOTEST-15: формат Test<Action>_<Condition>_<Expected> (PascalCase, Go-конвенция).
func TestCreateOrder_WhenCustomerNotFound_Returns404(t *testing.T) { ... }
func TestCreateOrder_WhenAmountIsNegative_Returns400(t *testing.T) { ... }
func TestGetOrder_WhenNotFound_Returns404(t *testing.T) { ... }
func TestCancelOrder_WhenAlreadyCancelled_Returns422(t *testing.T) { ... }
func TestCreateProduct_Success(t *testing.T) { ... }
Комментарий с BR-кодом перед функцией — связывает тест со спецификацией:
// TestCreateOrder_WhenAmountIsNegative_Returns400 проверяет BR-ORDER-3:
// сумма заказа должна быть положительной.
func TestCreateOrder_WhenAmountIsNegative_Returns400(t *testing.T) {
...
}
require vs assert
GOTEST-16: два пакета testify с разным поведением при ошибке.
| Пакет | Поведение | Когда |
|---|---|---|
require.* | t.FailNow() — останавливает тест | критичные шаги setup и Act; нет смысла проверять Assert, если Act провалился |
assert.* | t.Fail() — продолжает тест | блок Assert — хотим видеть все расхождения сразу |
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusCreated, resp.StatusCode)
var body OrderResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
assert.Equal(t, "c1", body.CustomerID)
assert.NotEmpty(t, body.OrderID)
assert.Equal(t, clock.at.UTC(), body.CreatedAt.UTC())
t.Fatal напрямую — только в TestMain; в обычном тесте всегда require.*.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
time.Sleep(...) для ожидания в тесте | GOTEST-X1 | синхронный flow, детерминированные зависимости |
assert.Eventually(...) как способ «дождаться» | GOTEST-X1 | вызвать relay/worker явно |
time.Now() в доменном коде напрямую | GOTEST-X2 | интерфейс Clock, в тесте fixedClock |
uuid.New() в Handler/Service/Aggregate | GOTEST-X2 | интерфейс IDGenerator, в тесте seqIDGenerator |
| Один тест на весь жизненный цикл | GOTEST-3 | один тест = один сценарий |
| Kafka-контейнер в базовом integration-тесте | GOTEST-X7 | Outbox-таблица через DatabasePreparer |
ioutil.ReadAll (устарел с Go 1.16) | GOTEST-X6 | io.ReadAll |
fmt.Println для отладки в тесте | GOTEST-X6 | t.Logf(...) — видно только при -v |
Куда дальше
- Инфраструктура теста — TestMain и Testcontainers —
TestMain,newTestServer, контейнер. - DatabasePreparer — fluent setup БД:
Clear,Create*,Find*. - Детерминизм — Clock и IDGenerator — интерфейсы и тестовые реализации.
- Один тест — структура, имена,
requirevsassert. - Kafka, Redis, async — по умолчанию НЕТ — Outbox-подход.
- Пирамида тестов — что чем покрывать: unit агрегата, unit контроллера, интеграционный, E2E.