Как тестировать 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 или другом.
  • Обработка ошибок: Как система ведет себя при неверных входных данных.
  • Время ответа: Убедиться, что запросы выполняются за приемлемое время.

Ответ 18+ 🔞

А, тестирование HTTP в Go, говоришь? Ну это ж классика, блядь, как борщ со сметаной! Слушай сюда, сейчас разложу по полочкам, а то народ иногда путается, как слепой кот в сортире.

Вот смотри, вся эта движуха делится на две большие, блядь, категории, как будто ты в магазине: одна полка — это твой сервер (хендлеры, которые запросы жрут), а вторая — клиент (который сам на внешние API лает). И для каждой есть свои, мать их, инструменты в стандартной библиотеке, не надо никаких левых фреймворков!

Первое — тестируем хендлеры, не поднимая весь сервер.

Представь, что тебе нужно проверить одну конкретную функцию, которая http.ResponseWriter и http.Request принимает. Запускать для этого целое приложение — это как из пушки по воробьям, ёпта. Нафиг надо? Для этого есть net/http/httptest.

Вот смотри, как это просто, в рот меня чих-пых:

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)
    }
}

Видишь? Никакой магии. Создали бутафорский запрос, подсунули его хендлеру вместе с подсадной уткой для записи ответа, и потом просто смотрим, что он нам наваял. Всё изолированно, быстро, красиво. Не хуй собачий!

Второе — тестируем клиента, который куда-то ходит.

А вот это, блядь, уже интереснее. Твой код, допустим, стучится к какому-то внешнему API за данными. И если в тестах он будет реально туда лезть — это пиздец, а не тесты. Сеть может лежать, API может сдохнуть, лимиты кончатся... Да и вообще, тесты не должны от внешнего мира зависеть, это же аксиома, ёпта!

Что делаем? Поднимаем свой, локальный, временный сервачок прямо в тесте! И он будет отдавать именно те ответы, которые мы ему пропишем. Вообще красота!

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. Дёргаем нашу функцию, но кидаем ей адрес нашего подставного сервера
    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)
    }
}

Вот и вся магия, блядь! Сервер создали, он отдал что надо, функция отработала, мы проверили. Никаких зависимостей от внешнего мира. Идиллическая картина, ёбана!

Ну и конечно, ручное тестирование.

Это когда уже всё собрано и нужно ткнуть пальцем, жив ли сервис. Тут уже в ход идут Postman, curl или что там у тебя любимое. Просто дёргаешь эндпоинты и смотришь, не несёт ли он хуйню в ответе.

На что вообще смотреть нужно, когда тестируешь HTTP-взаимодействия:

  • Статус-код, ёпта! 200, 404, 500 — это основа основ. Если ожидал 201 Created, а получил 400 Bad Request — это повод задуматься, а не обосрался ли твой код.
  • Заголовки (Headers). Особенно Content-Type. Отправил JSON, а в заголовках text/plain — пидарас, извини.
  • Тело ответа (Response Body). Данные должны быть в правильном формате (JSON, XML) и структуре. Если ждёшь {"name": "Vasya"}, а приходит {"username": "Petya"}, то что-то пошло не так, блядь.
  • Обработка ошибок. А что будет, если отправить полную хуйню вместо JSON? Упадёт с паникой или красиво ответит 400 с описанием? Это важно!
  • Скорость. Ну это уже для перфекционистов, но если запрос выполняется 10 секунд, то это тоже, мягко говоря, не айс.

Вот так вот, коротко и по делу. Главное — разделяй тесты на изолированные модульные и интеграционные, где поднимаешь части системы. И не усложняй там, где не надо, а то сам потом в своём коде разбираться не сможешь, как мартышка в апельсинах!