Тестирование в Go встроено в язык и инструменты: пакет testing в стандартной библиотеке, команда go test, никакого внешнего раннера. А подменяемость зависимостей даёт не контейнер, а интерфейсы — маленькие, объявленные там, где используются. Это делает тесты быстрыми и явными.
Пакет testing и table-driven
Тест — функция TestXxx(t *testing.T). Идиоматичная форма — table-driven: набор случаев в срезе, один цикл.
func TestMapError(t *testing.T) {
cases := []struct {
name string
err error
want int
}{
{"not found", ErrProductNotFound, http.StatusNotFound},
{"forbidden", ErrForbidden, http.StatusForbidden},
{"other", errors.New("boom"), http.StatusInternalServerError},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := mapError(tc.err); got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}
}
t.Run даёт каждому случаю имя и независимый прогон. Это нижний, самый широкий уровень пирамиды — быстрые тесты чистой логики.
httptest для обработчиков
HTTP-слой тестируют без сети — пакетом httptest. NewRecorder ловит ответ обработчика, NewServer поднимает реальный сервер в памяти для сквозных проверок.
func TestGetProduct(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/products/1", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status: got %d, want 200", rec.Code)
}
}
Так проверяется весь конвейер — middleware, роутинг, обработчик — на настоящем http.Handler.
Подмена через интерфейсы
В Go зависимости подменяют не контейнером, а интерфейсами. Ключевая идиома: интерфейс объявляет потребитель, и он маленький — ровно те методы, что нужны.
type productSaver interface {
Save(ctx context.Context, cmd CreateProductCommand) (Product, error)
}
type CreateProductHandler struct {
repo productSaver
}
Теперь в тесте вместо реального репозитория подставляют заглушку, реализующую productSaver:
type stubSaver struct{ saved Product }
func (s *stubSaver) Save(ctx context.Context, cmd CreateProductCommand) (Product, error) {
return s.saved, nil
}
Handler тестируется в изоляции, без базы — быстро. Маленькие интерфейсы у потребителя — то, что делает Go-код тестируемым без фреймворка моков.
Реальная база: testcontainers
Тесты, которым нужна база, бьют в настоящий PostgreSQL (диалект и SQL должны быть теми же), поднятый на время теста через testcontainers. Так проверяются sqlc-запросы и миграции — на реальной базе, а не на имитации. Таких тестов держат немного: они медленнее и стоят выше в пирамиде.
Где это в UCP
Интерфейсы у потребителя задают пирамиду: быстрые тесты Handler-ов и домена с заглушками — снизу, тесты обработчиков через httptest — посередине, немного сквозных с testcontainers — сверху. Логику не гоняют через HTTP без нужды, HTTP-слой не дублируют. Это та же стратегия, что в Spring-биндинге со слайс-тестами и TestContainers, только стандартной библиотекой Go и маленькими интерфейсами. Сервис, который так тестируется, продукт-инженер меняет без страха — go test даёт ответ за секунды.