Как тестировать HTTP-хендлеры и API-клиенты в Go?

Ответ

Тестирование HTTP-взаимодействий в Go можно разделить на две основные категории: тестирование сервера (ваших хендлеров) и тестирование клиента (кода, который обращается к внешним API).

1. Модульное тестирование HTTP-серверов (хендлеров)

Для тестирования хендлеров в изоляции, без запуска реального веб-сервера, используется стандартный пакет net/http/httptest.

  • httptest.NewRequest(): Создает mock-запрос (*http.Request) для передачи в ваш хендлер.
  • httptest.NewRecorder(): Создает mock-объект http.ResponseWriter, который записывает код ответа, заголовки и тело, чтобы вы могли их проверить.
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

// Тестируемый хендлер
func MyHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func TestMyHandler(t *testing.T) {
    // 1. Создаем запрос к нашему хендлеру
    req := httptest.NewRequest(http.MethodGet, "/test", nil)

    // 2. Создаем ResponseRecorder для записи ответа
    rr := httptest.NewRecorder()

    // 3. Вызываем хендлер напрямую
    MyHandler(rr, req)

    // 4. Проверяем результат
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }

    expected := `OK`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
    }
}

2. Модульное тестирование HTTP-клиентов

Когда ваш код делает запросы к внешним сервисам, тесты не должны зависеть от доступности этих сервисов. Для этого используется httptest.NewServer, который запускает настоящий, но локальный и временный HTTP-сервер для ваших тестов.

package main

import (
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
)

// Функция, которую мы тестируем (она делает внешний запрос)
func GetUserData(apiURL string) (string, error) {
    resp, err := http.Get(apiURL + "/users/1")
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)
    return string(body), nil
}

func TestGetUserData(t *testing.T) {
    // 1. Создаем тестовый сервер
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Проверяем, что клиент пришел по правильному пути
        if r.URL.Path != "/users/1" {
            t.Errorf("Expected to request '/users/1', got '%s'", r.URL.Path)
        }
        // Отдаем заранее подготовленный ответ
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"id": 1, "name": "John Doe"}`))
    }))
    defer server.Close() // Важно закрыть сервер после теста

    // 2. Вызываем нашу функцию, передав ей URL тестового сервера
    body, err := GetUserData(server.URL)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    // 3. Проверяем, что функция правильно обработала ответ
    expected := `{"id": 1, "name": "John Doe"}`
    if body != expected {
        t.Errorf("unexpected body: got %s want %s", body, expected)
    }
}

3. Ручное и интеграционное тестирование

Для проверки работающего приложения используются инструменты вроде Postman, Insomnia или консольной утилиты curl. Они позволяют отправлять реальные запросы к запущенному приложению и анализировать ответы.

Ключевые аспекты для проверки:

  • Код состояния HTTP (Status Code): 200 OK, 404 Not Found, 500 Internal Server Error и т.д.
  • Заголовки (Headers): Content-Type, Authorization и другие.
  • Тело ответа (Response Body): Корректность данных в формате JSON, XML или другом.
  • Обработка ошибок: Как система ведет себя при неверных входных данных.
  • Время ответа: Убедиться, что запросы выполняются за приемлемое время.