Тестирование HTTP-обработчиков через httptest
Содержание курса
Проверка статуса, Content-Type, декодированного JSON и мутационная верификация теста
Статус проверяется прямолинейно: resp.StatusCode != tc.wantStatus. Интереснее три класса проверок тела — успешный ответ, ответ об ошибке и пустое тело.
Успешный ответ. POST возвращает созданную задачу, GET возвращает список. Декодируем и сверяем конкретные поля:
checkBody: func(t *testing.T, resp *http.Response) {
var task Task
if err := json.NewDecoder(resp.Body).Decode(&task); err != nil {
t.Fatalf("decode: %v", err)
}
if task.Title != "buy milk" {
t.Errorf("title: got %q", task.Title)
}
if task.ID == 0 {
t.Error("ID must be non-zero")
}
},
Ответ об ошибке. Для 400, 404, 405 API возвращает {"error": "..."}. Нас не интересует конкретный текст — формулировка может измениться. Проверяем только то, что поле непустое:
checkBody: func(t *testing.T, resp *http.Response) {
var e struct{ Error string `json:"error"` }
if err := json.NewDecoder(resp.Body).Decode(&e); err != nil {
t.Fatalf("decode error response: %v", err)
}
if e.Error == "" {
t.Error("expected non-empty error field")
}
},
Такой тест не сломается при рефакторинге текста сообщений, но сломается, если обработчик перестанет возвращать JSON вообще.
Пустое тело (DELETE 204). После успешного удаления тело должно быть пустым. Функция checkBody получает resp *http.Response, поэтому читаем resp.Body напрямую:
checkBody: func(t *testing.T, resp *http.Response) {
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
if len(body) != 0 {
t.Errorf("expected empty body, got %d bytes: %q", len(body), body)
}
},
Важно: для DELETE случай не устанавливает wantCT: true, поэтому проверка Content-Type не выполняется — 204 не предполагает тела и заголовка типа содержимого.
Мутационная проверка. После того как go test ./... даёт PASS по всем подтестам, нужно убедиться, что тесты реально что-то проверяют, а не просто проходят. Для этого намеренно ломаем одну деталь обработчика.
Пример 1: обработчик POST возвращает http.StatusCreated (201). Меняем на http.StatusOK (200):
// было
w.WriteHeader(http.StatusCreated)
// стало (мутация)
w.WriteHeader(http.StatusOK)
Подтест "POST valid" должен упасть с сообщением вида status: got 200, want 201. Остальные подтесты остаются зелёными.
Пример 2: ветка невалидного JSON — было writeError(w, 400, "invalid json"), стало голый return:
// мутация
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
return // без ответа
}
Recorder запишет статус 200 (дефолт) и пустое тело. Подтест "POST invalid JSON" ожидает статус 400 и JSON с непустым error — оба ассерта падают.
Важно выбирать мутацию, которая точечно меняет контракт. Если удалить только вызов декодирования, но следующая валидация title == "" всё равно вернёт 400 — мутация не делает тест красным. Это признак слабого теста: нужно либо усилить checkBody так, чтобы он различал «400 из-за невалидного JSON» и «400 из-за пустого title», либо выбрать другую точку для мутации.
