httpmock для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

httpmock для Go: полное руководство с примерами

1. Что это и зачем нужна

Библиотека httpmock для языка Go предоставляет мощный и гибкий инструмент для мокирования (имитации) HTTP-запросов и ответов в тестах. В современной разработке программного обеспечения взаимодействие с внешними API, веб-сервисами и микросервисами является неотъемлемой частью. Однако тестирование кода, который напрямую обращается к этим внешним ресурсам, сопряжено с рядом проблем: зависимость от доступности сети, медленная скорость выполнения тестов, непредсказуемость ответов реальных серверов и невозможность воспроизведения граничных случаев (например, таймаутов или специфических кодов ошибок). httpmock решает эти проблемы, позволяя разработчикам перехватывать HTTP-запросы на уровне транспорта Go и возвращать заранее заданные ответы, полностью изолируя тестируемый код от внешнего мира.

Основная цель httpmock — сделать юнит-тесты и интеграционные тесты, связанные с HTTP-клиентами, быстрыми, надежными и детерминированными. Вместо того чтобы отправлять реальные запросы в интернет, библиотека подменяет стандартный HTTP-транспорт Go (http.RoundTripper) на свой собственный, который сопоставляет входящие запросы с набором зарегистрированных "заглушек" (stubs). Каждая заглушка определяет условия, которым должен удовлетворять запрос (метод, URL, заголовки, тело), и ответ, который должен быть возвращен. Это позволяет тестировать логику обработки ответов, повторные попытки, обработку ошибок и другие аспекты без необходимости поднимать реальные серверы.

httpmock особенно полезна в следующих сценариях: тестирование клиентов к REST API, тестирование сервисов, которые агрегируют данные из нескольких внешних источников, тестирование обработчиков вебхуков, а также при разработке в условиях ограниченного или нестабильного интернет-соединения. Благодаря своей простоте и интеграции с пакетом net/http, httpmock является стандартом де-факто для мокирования HTTP в Go-сообществе.

2. Установка

Установка httpmock производится стандартным для Go способом с помощью команды go get. Убедитесь, что у вас установлен Go версии 1.11 или выше (для поддержки модулей).

go get github.com/jarcoal/httpmock

После выполнения этой команды библиотека будет добавлена в ваш go.mod файл. Для обновления до последней версии используйте:

go get -u github.com/jarcoal/httpmock

Теперь вы можете импортировать библиотеку в свои тестовые файлы:

import "github.com/jarcoal/httpmock"

3. Быстрый старт — минимальный рабочий пример

Давайте создадим простой тест, который проверяет функцию, делающую GET-запрос к внешнему API и возвращающую тело ответа. Мы замокируем этот запрос, чтобы тест выполнялся без реального обращения к сети.

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

// FetchData делает GET-запрос по указанному URL и возвращает тело ответа.
func FetchData(url string) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

func main() {
    // Это просто демонстрация, в реальности функция будет вызываться из тестов.
    data, err := FetchData("https://api.example.com/data")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Data:", data)
}

Теперь напишем тест с использованием httpmock:

package main

import (
    "testing"
    "github.com/jarcoal/httpmock"
)

func TestFetchData(t *testing.T) {
    // 1. Активируем мокирование. Все HTTP-запросы будут перехватываться.
    httpmock.Activate()
    // 2. Обязательно деактивируем мокирование после теста, чтобы восстановить оригинальный транспорт.
    defer httpmock.DeactivateAndReset()

    // 3. Регистрируем заглушку: на GET-запрос к https://api.example.com/data вернуть статус 200 и тело "mocked response".
    httpmock.RegisterResponder("GET", "https://api.example.com/data",
        httpmock.NewStringResponder(200, "mocked response"))

    // 4. Вызываем тестируемую функцию.
    result, err := FetchData("https://api.example.com/data")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    // 5. Проверяем результат.
    expected := "mocked response"
    if result != expected {
        t.Errorf("expected %q, got %q", expected, result)
    }
}

В этом примере httpmock.Activate() заменяет глобальный HTTP-транспорт на мокированный. RegisterResponder создает правило, которое говорит: "Если придет GET-запрос на этот URL, верни ответ с кодом 200 и телом 'mocked response'". Тест выполняется мгновенно и не зависит от сети.

4. Основные методы/функции/классы

Рассмотрим ключевые функции и методы библиотеки httpmock. Все они являются функциями пакета httpmock.

4.1. Activate

Сигнатура: func Activate()

Что делает: Активирует режим мокирования, заменяя глобальный транспорт http.DefaultTransport на мокированный. Все последующие HTTP-запросы, сделанные через стандартный http.Client, будут перехватываться. Должна вызываться в начале теста.

func TestActivateExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()
    // ... тесты
}

4.2. DeactivateAndReset

Сигнатура: func DeactivateAndReset()

Что делает: Деактивирует режим мокирования, восстанавливая оригинальный транспорт, и сбрасывает все зарегистрированные заглушки и счетчики вызовов. Рекомендуется вызывать в defer сразу после Activate().

func TestDeactivateExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()
    // ... тесты
}

4.3. RegisterResponder

Сигнатура: func RegisterResponder(method, url string, responder Responder)

Что делает: Регистрирует заглушку для HTTP-запроса с указанным методом (GET, POST, PUT и т.д.) и URL. Когда поступает запрос, соответствующий этим параметрам, вызывается переданный responder, который генерирует ответ. URL может содержать подстановочные знаки (например, https://api.example.com/*).

func TestRegisterResponderExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()

    httpmock.RegisterResponder("GET", "https://api.example.com/users/1",
        httpmock.NewStringResponder(200, `{"id": 1, "name": "Alice"}`))

    // Теперь запрос к этому URL вернет заданный JSON.
}

4.4. NewStringResponder

Сигнатура: func NewStringResponder(status int, body string) Responder

Что делает: Создает простой Responder, который возвращает HTTP-ответ с заданным статус-кодом и телом в виде строки. Это самый распространенный способ создания ответов.

func TestNewStringResponderExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()

    // Возвращаем статус 201 и тело "Created"
    responder := httpmock.NewStringResponder(201, "Created")
    httpmock.RegisterResponder("POST", "https://api.example.com/items", responder)
}

4.5. NewBytesResponder

Сигнатура: func NewBytesResponder(status int, body []byte) Responder

Что делает: Аналогичен NewStringResponder, но принимает тело ответа в виде среза байтов ([]byte). Полезен для возврата бинарных данных, таких как изображения или сжатые данные.

func TestNewBytesResponderExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()

    imageBytes := []byte{0x89, 0x50, 0x4E, 0x47} // Пример PNG заголовка
    responder := httpmock.NewBytesResponder(200, imageBytes)
    httpmock.RegisterResponder("GET", "https://example.com/image.png", responder)
}

4.6. NewJsonResponder

Сигнатура: func NewJsonResponder(status int, body interface{}) (Responder, error)

Что делает: Создает Responder, который сериализует переданный объект (структуру, карту, срез) в JSON и возвращает его в качестве тела ответа. Устанавливает заголовок Content-Type: application/json. Возвращает ошибку, если сериализация не удалась.

func TestNewJsonResponderExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()

    user := map[string]interface{}{"id": 1, "name": "Bob"}
    responder, err := httpmock.NewJsonResponder(200, user)
    if err != nil {
        t.Fatalf("failed to create JSON responder: %v", err)
    }
    httpmock.RegisterResponder("GET", "https://api.example.com/user", responder)
}

4.7. NewNotFoundResponder

Сигнатура: func NewNotFoundResponder() Responder

Что делает: Создает Responder, который возвращает стандартный ответ HTTP 404 Not Found. Полезен для тестирования обработки ошибок, когда запрос не соответствует ни одной зарегистрированной заглушке.

func TestNewNotFoundResponderExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()

    // Если запрос не соответствует ни одной заглушке, по умолчанию возвращается 404.
    // Этот responder можно использовать для явной регистрации такого поведения.
    httpmock.RegisterResponder("GET", "https://api.example.com/unknown",
        httpmock.NewNotFoundResponder())
}

4.8. NewErrorResponder

Сигнатура: func NewErrorResponder(err error) Responder

Что делает: Создает Responder, который имитирует ошибку транспорта (например, таймаут соединения или разрыв связи). Возвращаемая ошибка будет передана вызывающему коду как ошибка выполнения HTTP-запроса. Критически важно для тестирования обработки сетевых ошибок.

func TestNewErrorResponderExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()

    // Имитируем ошибку соединения
    httpmock.RegisterResponder("GET", "https://api.example.com/timeout",
        httpmock.NewErrorResponder(fmt.Errorf("connection timeout")))
}

4.9. RegisterRegexpResponder

Сигнатура: func RegisterRegexpResponder(method string, urlRegex *regexp.Regexp, responder Responder)

Что делает: Регистрирует заглушку, которая срабатывает, если URL запроса соответствует заданному регулярному выражению. Это более гибкий способ, чем использование подстановочных знаков в строке URL.

func TestRegisterRegexpResponderExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()

    // Регистрируем заглушку для всех запросов, содержащих "/users/" в URL
    re := regexp.MustCompile(`https://api\.example\.com/users/\d+`)
    httpmock.RegisterRegexpResponder("GET", re,
        httpmock.NewStringResponder(200, `{"role": "user"}`))
}

4.10. GetCallCountInfo

Сигнатура: func GetCallCountInfo() map[string]int

Что делает: Возвращает карту, где ключом является строка вида "METHOD URL" (например, "GET https://api.example.com/data"), а значением — количество раз, которое был вызван соответствующий responder. Позволяет проверять, сколько раз были сделаны те или иные запросы.

func TestGetCallCountInfoExample(t *testing.T) {
    httpmock.Activate()
    defer httpmock.DeactivateAndReset()

    httpmock.RegisterResponder("GET", "https://api.example.com/data",
        httpmock.NewStringResponder(200, "ok"))

    // Делаем запрос дважды
    http.Get("https://api.example.com/data")
    http.Get("https://api.example.com/data")

    callCount := httpmock.GetCallCountInfo()
    if callCount["GET https://api.example.com/data"] != 2 {
        t.Errorf("expected 2 calls, got %d", callCount["GET https://api.example.com/data"])
    }
}

5. Реальный практический пример (100-200 строк кода)

Рассмотрим более сложный пример: сервис, который получает информацию о погоде из внешнего API, кэширует ее и предоставляет пользователю. Мы протестируем основной сценарий,

Рекомендаций