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, кэширует ее и предоставляет пользователю. Мы протестируем основной сценарий,