3. Middleware тестирование
Первая линия обороны
В предыдущей главе 2. Тестирование handler функций мы рассматривали тестирование прикладных обработчиков в предположении, что входящий запрос уже благополучно преодолел сетевой периметр и доставлен прямо к порогу бизнес-логики. Однако в архитектуре реальных production-систем хэндлер — это лишь внутренняя сердцевина слоеной луковицы. Прежде чем поток управления коснется контроллера, входящий HTTP-пакет проходит через эшелонированную цепочку промежуточного программного обеспечения — Middleware.
Мидлвари реализуют классический архитектурный паттерн Chain of Responsibility (Цепочка обязанностей). На их плечи ложится вся сквозная инфраструктурная забота:
- Проверка аутентификации и декодирование JWT-токенов.
- Авторизация и разграничение прав доступа (RBAC).
- Структурированное логирование и распределенный трейсинг (OpenTelemetry / W3C TraceContext).
- Защита от перегрузок и атак типа DDoS (Rate Limiting).
- Восстановление после аварийных ситуаций (Panic Recovery).
- Обогащение контекста запроса метаданными (Correlation ID, профиль пользователя).
Масштаб ответственности колоссален: ошибка в прикладном хэндлере затронет один конкретный сценарий одного пользователя. Ошибка в слое Auth-мидлвари приведет к катастрофической утечке конфиденциальных данных или полному отказу (Downtime) всей платформы. Поэтому изоляция и строгое модульное тестирование каждого звена этой цепочки — первостепенная инженерная обязанность.
Архитектура Middleware в Go
Канонический мидлварь в стандартной библиотеке Go представляет собой функцию высшего порядка с предельно выразительной сигнатурой: она принимает следующий по цепочке интерфейс http.Handler и возвращает новый обернутый http.Handler (как правило, используя адаптер http.HandlerFunc):
type Middleware func(http.Handler) http.Handler
flowchart LR
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
Client["Клиентский HTTP Запрос"]:::entry -->|"Транспорт"| MW["Middleware (Auth / Logging)"]
MW -->|"Успех: next.ServeHTTP(w, r)"| Handler["Прикладной Handler"]
MW -.->|"Ошибка 401: Short-circuit (return)"| Client
Handler -->|"HTTP 200 OK"| MW
MW -->|"Финальный ответ"| Client
Любой промежуточный обработчик реализует один из двух сценариев поведения:
- Сквозной пропуск (Pass-through): Запрос полностью валиден. Мидлварь обогащает контекст (
r.WithContext(ctx)) и делегирует выполнение следующему звену вызовомnext.ServeHTTP(w, r). - Короткое замыкание (Short-circuit): Запрос не прошел проверку (отсутствует токен, превышена квота запросов). Мидлварь фиксирует код ошибки в объекте
w, отправляет ответ клиенту и немедленно прерывает исполнение операторомreturn, не позволяя запросу дойти до бизнес-кода.
Стратегия тестирования: Dummy Handler
Как протестировать отдельный мидлварь в строгой изоляции — без подключения реальной базы данных, без развертывания роутера и без вызова тяжелых прикладных сервисов?
Для этого применяется проверенный паттерн Dummy Handler (Фиктивный обработчик-шпион).
Идея заключается в создании минимальной заглушки для аргумента next. Этот фиктивный хэндлер выполняет единственную задачу: он фиксирует факт своего вызова, изменяя булеву переменную (wasCalled = true), и при необходимости сохраняет данные из контекста для последующей проверки.
Обернув эту тестовую заглушку в наш мидлварь, мы запускаем его через httptest и математически строго проверяем:
- Был ли передан контроль дальше при валидных данных?
- Была ли цепочка жестко разорвана при ошибочном запросе?
Реализация: Auth Middleware
Спроектируем компактный, но строгий мидлварь проверки Bearer-токена:
package api
import (
"context"
"net/http"
"strings"
)
// Кастомный строковый тип для ключа контекста исключает коллизии в сторонних пакетах
type contextKey string
const UserIDKey contextKey = "userID"
// AuthMiddleware проверяет авторизационный заголовок и внедряет UserID в контекст
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return // КРИТИЧЕСКИ ВАЖНО: немедленный возврат!
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" || parts[1] != "secret-token" {
http.Error(w, "invalid token", http.StatusUnauthorized)
return // Прерываем цепочку
}
// Создаем новый контекст, содержащий извлеченный идентификатор
ctx := context.WithValue(r.Context(), UserIDKey, 42)
// Делегируем выполнение дальше, передавая запрос с обновленным контекстом
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Пишем Table-Driven Test
Воспользуемся инструментарием httptest (детально изученным в 1. net/http/httptest пакет) и организуем исчерпывающий табличный сьют (4. Table driven tests):
package api_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
"yourproject/internal/api"
)
func TestAuthMiddleware(t *testing.T) {
t.Parallel()
// 1. Описание таблицы тестовых сценариев
type testCase struct {
name string
authHeader string
expectedStatus int
expectedNext bool // Ожидается ли передача управления следующему звену?
}
testCases := []testCase{
{
name: "Успешная авторизация по валидному Bearer токену",
authHeader: "Bearer secret-token",
expectedStatus: http.StatusOK,
expectedNext: true,
},
{
name: "Отказ: Заголовок Authorization полностью отсутствует",
authHeader: "",
expectedStatus: http.StatusUnauthorized,
expectedNext: false,
},
{
name: "Отказ: Передан некорректный секретный токен",
authHeader: "Bearer hacker-token",
expectedStatus: http.StatusUnauthorized,
expectedNext: false,
},
{
name: "Отказ: Неверная схема авторизации (Basic вместо Bearer)",
authHeader: "Basic secret",
expectedStatus: http.StatusUnauthorized,
expectedNext: false,
},
}
for _, tc := range testCases {
tc := tc // Захват переменной цикла для параллельного запуска
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// 1. Инициализируем Dummy Handler-шпион
var nextCalled bool
var contextUserID any
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
// Извлекаем значение из контекста для строгой верификации
contextUserID = r.Context().Value(api.UserIDKey)
w.WriteHeader(http.StatusOK)
})
// 2. Оборачиваем шпиона в тестируемый мидлварь
handlerToTest := api.AuthMiddleware(nextHandler)
// 3. Формируем входящий запрос и рекордер
req := httptest.NewRequest(http.MethodGet, "/secure-resource", nil)
if tc.authHeader != "" {
req.Header.Set("Authorization", tc.authHeader)
}
rec := httptest.NewRecorder()
// 4. Act: Запускаем исполнение цепочки в памяти
handlerToTest.ServeHTTP(rec, req)
// 5. Assert: Верифицируем сетевой контракт
res := rec.Result()
defer res.Body.Close()
require.Equal(t, tc.expectedStatus, res.StatusCode)
// КЛЮЧЕВАЯ ПРОВЕРКА: сработал ли Short-circuit?
require.Equal(t, tc.expectedNext, nextCalled,
"Поведение передачи управления в next.ServeHTTP не соответствует спецификации")
// Проверяем корректность инъекции контекста при успешном сценарии
if tc.expectedNext {
require.NotNil(t, contextUserID, "UserID обязан присутствовать в контексте")
require.Equal(t, 42, contextUserID)
}
})
}
}
Порядок мидлварей (Chain Ordering)
В реальных приложениях мидлвари собираются в длинные конвейеры с помощью мультиплексоров (например, метод chi.Use в роутере Chi) или вложенных функциональных композиций вида Log(Recover(Auth(handler))).
Хотя юнит-тесты изолированных мидлварей подтверждают надежность каждого блока по отдельности, они не гарантируют целостности собранной воедино цепочки. Чтобы убедиться, что роутер, стек мидлварей и контроллеры взаимодействуют гармонично, необходимо переходить к тестированию всего REST API в сборе.
В следующей лекции мы объединим все компоненты воедино и разберем сквозное тестирование контрактов: 4. Тестирование REST API.