Опирается на правила: 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-роутер)
PostgreSQLTestcontainers postgres.Run(ctx, "postgres:16-alpine")
HTTP-клиентhttp.DefaultClient.Do(req)
Внешние HTTPотдельный httptest.NewServer с http.HandlerFunc-заглушкой
KafkaНЕ поднимаем; проверяем Outbox-таблицу через DatabasePreparer
RedisНЕ поднимаем; подменяем на NoopCache build-тегом
ВремяfixedClock — реализация интерфейса Clock
UUIDseqIDGenerator — реализация интерфейса 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.

Корректно — синхронный поток:

  1. Clock через интерфейс → в тесте fixedClock с заданным значением.
  2. IDGenerator через интерфейс → в тесте seqIDGenerator (счётчик).
  3. Outbox-relay вызывается явно: relay.ProcessPending(ctx), не ждём фонового тика.
  4. 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/AggregateGOTEST-X2интерфейс IDGenerator, в тесте seqIDGenerator
Один тест на весь жизненный циклGOTEST-3один тест = один сценарий
Kafka-контейнер в базовом integration-тестеGOTEST-X7Outbox-таблица через DatabasePreparer
ioutil.ReadAll (устарел с Go 1.16)GOTEST-X6io.ReadAll
fmt.Println для отладки в тестеGOTEST-X6t.Logf(...) — видно только при -v

Куда дальше

  • Инфраструктура теста — TestMain и Testcontainers — TestMain, newTestServer, контейнер.
  • DatabasePreparer — fluent setup БД: Clear, Create*, Find*.
  • Детерминизм — Clock и IDGenerator — интерфейсы и тестовые реализации.
  • Один тест — структура, имена, require vs assert.
  • Kafka, Redis, async — по умолчанию НЕТ — Outbox-подход.
  • Пирамида тестов — что чем покрывать: unit агрегата, unit контроллера, интеграционный, E2E.