10. Wiremock и HTTP mocking
Управление хаосом: Когда httptest.Server уже недостаточно
В предыдущей главе 9. Тестирование внешних API мы сформулировали фундаментальный принцип: мокирование интерфейсов сетевых клиентов маскирует ошибки сериализации и протокола, поэтому идиоматичный подход в Go — запуск локального веб-сервера через httptest.NewServer с переопределением базового адреса (BaseURL).
Этот подход безупречно справляется со своей задачей, пока ваш сервис взаимодействует с одним или двумя простыми эндпоинтами. Однако в реальной enterprise-архитектуре сервисы редко бывают столь компактными.
Представьте типичный оркестратор заказов в электронной коммерции:
- Он проверяет JWT-токен в сервисе авторизации (
/oauth/verify). - Запрашивает резервирование товаров на складе (
/inventory/reserve). - Обращается к платежному шлюзу для холдирования средств (
/v1/payments). - Формирует накладную в CRM-системе.
- Инициирует отправку уведомления клиенту через внешний шлюз рассылок.
Если попытаться сымитировать все эти интеграции через один самописный httptest.Server, функция http.HandlerFunc стремительно деградирует в гигантский, трудноподдерживаемый лабиринт из громоздких конструкций switch-case по путям запросов, ручного парсинга заголовков и сопоставления JSON-схем. Разработчик перестает писать тесты и начинает с нуля изобретать собственный несовершенный mock-сервер.
Для управления подобной сложностью в индустрии выработан стандарт декларативного мокирования протокола HTTP (Declarative HTTP Mocking), лидером которого является WireMock.
Что такое WireMock?
WireMock — это автономный, промышленный HTTP-сервер, поведение которого описывается не императивным процедурным кодом, а декларативными правилами соответствия (Stubs & Matchers) в формате JSON или через типизированный программный API.
Вместо написания логики роутинга инженер формулирует четкое правило-контракт:
«Если на сервер поступает запрос с методом POST на путь /v1/payments, в заголовках присутствует Authorization: Bearer X, а внутри тела запроса JSONPath-выражение $.amount равно числу 100 — ответь со статусом 201 Created, задержкой в 200 миллисекунд и строго определенным JSON-телом ответа».
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph TestApp["Тестируемый Go-сервис"]
Client["http.Client (Go)"]
end
subgraph WireMockEngine["WireMock Server (Docker Container)"]
Router["HTTP Listener & Request Router"]:::entry
subgraph MatchingPipeline["Движок сопоставления (Radix Tree + JSONPath)"]
M1["Matcher 1: Path /v1/payments<br>Header: Authorization: Bearer X<br>JSONPath: $.amount == 100"]:::process
M2["Matcher 2: Path /v1/payments<br>Fault: ConnectionReset"]:::process
MDefault["Fallthrough: Запрос не распознан"]:::entry
end
Router --> M1
Router --> M2
Router --> MDefault
Resp1["HTTP 201 Created<br>Status: approved"]:::network
Resp2["TCP RST (Connection Reset)"]
Resp404["HTTP 404 Not Found<br>WireMock Stub Mismatch Report"]:::error
M1 -->|"Match"| Resp1
M2 -->|"Match"| Resp2
MDefault -->|"No Match"| Resp404
end
Client -->|"POST /v1/payments"| Router
Resp1 -.->|"RESP"| Client
Resp2 -.->|"Reset"| Client
Интеграция WireMock в Go через Testcontainers
Идиоматичный способ интеграции WireMock в Go-проектах — запуск контейнера с помощью официального модуля из testcontainers-go в комбинации с клиентом управления github.com/wiremock/go-wiremock.
Создадим изолированную тестовую среду:
package integration_test
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/wiremock"
wiremock_client "github.com/wiremock/go-wiremock"
)
// setupWireMock разворачивает контейнер WireMock и инициализирует управляющий REST-клиент
func setupWireMock(t *testing.T) (*wiremock_client.Client, string) {
t.Helper()
ctx := context.Background()
// Поднимаем официальный легковесный контейнер WireMock
container, err := wiremock.RunContainer(ctx,
testcontainers.WithImage("wiremock/wiremock:3.3.1"),
)
require.NoError(t, err, "не удалось запустить контейнер WireMock")
// Регистрируем гарантированную остановку и удаление контейнера
t.Cleanup(func() {
_ = container.Terminate(ctx)
})
// Извлекаем динамический HTTP endpoint, назначенный Docker-демоном
url, err := container.PortEndpoint(ctx, "8080", "http")
require.NoError(t, err)
// Создаем Go-клиент для декларативной отправки стабов по HTTP API
client := wiremock_client.NewClient(url)
return client, url
}
А теперь напишем выразительный интеграционный тест, проверяющий обработку платежа с валидацией полезной нагрузки:
func TestBillingService_ProcessPayment_Success(t *testing.T) {
t.Parallel()
// Поднимаем инфраструктуру WireMock
wmClient, wmURL := setupWireMock(t)
// 1. Arrange: Декларативно конфигурируем ожидание внешнего запроса
stub := wiremock_client.Post(wiremock_client.URLPathEqualTo("/v1/payments")).
WithHeader("Authorization", wiremock_client.EqualTo("Bearer X")).
// Проверяем тело запроса через точечное JSONPath выражение
WithBodyPattern(wiremock_client.MatchesJsonPath("$.amount", wiremock_client.EqualTo("100"))).
WillReturn(
`{"id": "pay_987", "status": "approved"}`,
map[string]string{"Content-Type": "application/json"},
http.StatusCreated,
)
// Регистрируем стаб в движке WireMock
err := wmClient.StubFor(stub)
require.NoError(t, err)
// 2. Act: Инициализируем наш боевой платежный сервис с адресом мок-сервера
billingService := billing.NewService(wmURL, "Bearer X")
paymentID, err := billingService.CreatePayment(context.Background(), 100)
// 3. Assert: Верифицируем бизнес-результат
require.NoError(t, err)
require.Equal(t, "pay_987", paymentID)
// 4. Verification: WireMock позволяет строго проверить, что запрос действительно ушел по сети
verified, err := wmClient.Verify(stub.Request(), 1)
require.NoError(t, err)
require.True(t, verified, "Сервис обязан был совершить ровно один HTTP-вызов к /v1/payments")
}
Stateful сценарии: Тестирование ретраев
Одна из сложнейших задач при проверке распределенной логики — тестирование механизмов повторных попыток (Retries with Exponential Backoff).
Как надежно проверить, что клиент при первом вызове сталкивается с временной недоступностью шлюза (HTTP 503 Service Unavailable), выдерживает паузу, повторяет попытку и успешно получает результат со статусом 200 OK?
Написание такого сценария на базе стандартного httptest требует создания глобальных потокобезопасных счетчиков и мьютексов. В WireMock для этого существует нативная машина состояний — Scenarios.
func TestService_RetryOnTransientFailure(t *testing.T) {
t.Parallel()
wmClient, wmURL := setupWireMock(t)
// Шаг 1: Начальное состояние. Первый запрос завершается ошибкой 503
// и автоматически переключает сценарий в состояние "Retry_Success"
stubFail := wiremock_client.Get(wiremock_client.URLEqualTo("/api/data")).
InScenario("Retry_Scenario").
WhenScenarioStateIs(wiremock_client.ScenarioStateStarted).
WillSetStateTo("Retry_Success").
WillReturn("", nil, http.StatusServiceUnavailable)
// Шаг 2: Повторный запрос в состоянии "Retry_Success" отдает 200 OK
stubSuccess := wiremock_client.Get(wiremock_client.URLEqualTo("/api/data")).
InScenario("Retry_Scenario").
WhenScenarioStateIs("Retry_Success").
WillReturn(`{"data": "ok"}`, map[string]string{"Content-Type": "application/json"}, http.StatusOK)
require.NoError(t, wmClient.StubFor(stubFail))
require.NoError(t, wmClient.StubFor(stubSuccess))
// Исполняем клиентский код, снабженный логикой повторных попыток
service := client.NewDataService(wmURL)
data, err := service.FetchDataWithRetry(context.Background())
// Тест будет зеленым только в том случае, если клиент пережил 503 и повторил вызов!
require.NoError(t, err)
require.Equal(t, "ok", data.Status)
}
Fault Injection: Инженерия хаоса
В боевом окружении под управлением Linux и Kubernetes серверы не просто возвращают красивые HTTP-ошибки. Сеть ненадежна: сокеты сбрасываются ядром, оптоволокно переживают экскаваторы, а буферы переполняются.
WireMock поддерживает внедрение низкоуровневых транспортных аномалий (Fault Injection):
Эмуляция сетевого обрыва (Connection Reset by Peer):
// Настраиваем WireMock на мгновенный сброс TCP-соединения
stubChaos := wiremock_client.Get(wiremock_client.URLEqualTo("/api/unstable")).
WillReturnFault(wiremock_client.FaultConnectionResetByPeer)
require.NoError(t, wmClient.StubFor(stubChaos))
service := client.NewDataService(wmURL)
_, err := service.FetchData(context.Background())
// Наш клиент должен перехватить ошибку транспортного уровня и корректно вернуть ошибку
require.Error(t, err)
require.Contains(t, err.Error(), "connection reset by peer")
Итог раздела
Этой лекцией мы подводим черту под фундаментальным разделом «05. Интеграционные тесты».
За десять глав мы проделали колоссальный путь:
- Разобрали границы и философию интеграционного тестирования.
- Подчинили реляционные базы данных и транзакционную изоляцию через
ROLLBACK. - Освоили управление контейнерами в Go через
testcontainers-goдля PostgreSQL, Redis и шины сообщений Redpanda/Kafka. - Научились избегать гонок ребалансировки и отказались от хрупких пауз
time.Sleepв пользу детерминированного асинхронного поллинга (Eventually). - Протестировали сквозной вертикальный срез системы с реальными сетевыми сокетами ядра Linux через
httptest.NewServer. - Освоили декларативное мокирование распределенных микросервисов и инженерию хаоса с помощью WireMock.
Теперь в нашем арсенале есть все необходимые инструменты для тестирования сложнейших интеграций бэкенда. Но как устроена магия самого сетевого стека Go изнутри? Как работают мультиплексоры, хэндлеры и низкоуровневые хуки тестирования?
В следующем подразделе мы погрузимся в детальное анатомирование сетевого тестирования стандартной библиотеки: 1. net/http/httptest пакет.