Тестирование Golang с помощью httptest

Пакет httptest от Go— это полезный ресурс для тестирования golang — автоматизации процесса тестирования вашего сервера, чтобы убедиться, что ваш веб-сервер или REST API работают должным образом. Автоматизация тестирования сервера не только помогает вам проверить, работает ли ваш код должным образом; это также сокращает время, затрачиваемое на тестирование, и особенно полезно для регрессионного тестирования. Пакет httptest также полезен для тестирования HTTP-клиентов, которые отправляют исходящие HTTP-запросы к удаленным серверам.

Пакет httptest был в первую очередь создан для тестирования HTTP-обработчиков Golang с использованием net/http, с которым он работает без проблем. Его также можно продлить. httptest может служить полной заменой вашей сторонней интеграции в качестве заглушки, и его можно легко адаптировать к вашей локальной среде разработки во время тестирования.

В этой статье представлен обзор того, как использовать httptest для тестирования обработчиков Golang на веб-сервере или REST API, а также для тестирования HTTP-клиентов.

Что такое httptest

Как упоминалось ранее, httptest или полностью net/http/httptest — это стандартная библиотека для написания конструктивных и модульных тестов для ваших обработчиков. Он предоставляет способы имитировать запросы к вашим обработчикам HTTP и устраняет необходимость запуска сервера.

httptest — как это работает

Прежде чем рассматривать тестовый пакет, важно сначала понять, как работает сам пакет обработчика HTTP и как обрабатываются ваши HTTP-запросы.

Пакет обработчика HTTP

Пакет стандартной библиотеки http net/http имеет клиентский и серверный интерфейс. Сервер по сути представляет собой группу обработчиков. Когда вы отправляете запрос на конечную точку или путь к серверу, обработчик перехватывает этот запрос и на основе запроса возвращает конкретный ответ.

Интерфейс обработчика

Ниже представлен простой интерфейс обработчика (http.Handler):

type Handler interface {
   ServeHTTP(ResponseWriter, *Request)
}

ServeHTTP принимает ResponseWriter и Request. Объект Request содержит входящий HTTP-запрос от клиента, а ResponseWriter можно использовать для создания ответа.

Пример обработчика

Вот пример простого обработчика:

// With no ServeHTTP

func handler(w http.ResponseWriter, r *http.Request) {
   w.Write([]byte("Hello, World!"))
}

With ServeHTTP

type home struct {}

func (h *home) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   w.Write([]byte("Hello, World!"))
}

Первый способ более распространен, чем второй. Гораздо проще и понятнее объявить обработчик как функцию.

Когда обработчик принимает HTTP-запрос, вы можете составить HTTP-ответ после выполнения всех необходимых действий с помощью интерфейса ResponseWriter. Этот процесс может быть либо чтением из базы данных, сторонних сервисов, статических данных сервера, либо обработкой файла. В приведенном выше коде строка w.Write([]byte(“Hello, World!”)) отправляет ответ.

Проверьте HTTP-обработчики Golang с помощью httptest

Несмотря на то, что обработчики — это просто функции, вы не можете писать для них модульные тесты обычным способом. Помеха возникает из-за параметров функции-обработчика типов ResponseWriter и Request. Они создаются автоматически библиотекой http при получении запроса сервером.

Итак, как создать объект ResponseWriter и Request для проверки обработчика? Здесь на помощь приходит httptest.

httptest имеет два метода: NewRequest и NewRecorder, которые помогают упростить процесс запуска тестов для ваших обработчиков. NewRequest имитирует запрос, который будет использоваться для обслуживания вашего обработчика. NewRecorder — это замена ResponseWriter, которая используется для обработки и сравнения ответа HTTP с ожидаемым результатом.

Вот простой пример методов httptest, инструкции по печати кода состояния ответа HTTP:

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

func handler(w http.ResponseWriter, r *http.Request) {
   w.Write([]byte("Hello, Worldn"))
}

func main() {
   req := httptest.NewRequest("GET", "http://google.com", nil)

   w := httptest.NewRecorder()

   handler(w, req)

   resp := w.Result()

   fmt.Println(resp.StatusCode)
}

Разбивка по строкам

Строка ниже позволяет вам создать новый макет запроса для вашего сервера. Вы передаете его своему обработчику как объект запроса. Здесь указаны такие подробности, как метод запроса или параметры запроса:

req := httptest.NewRequest("GET", "http://google.com", nil)

Следующая строка — это ваш интерфейс ResponseWriter, в которой записаны все ответы обработчика:

w := httptest.NewRecorder()

Теперь вы можете использовать эти переменные для вызова функции-обработчика:

handler(w, req)

После выполнения запроса эти строки позволяют увидеть результаты и детали ответа HTTP на запрос:

resp := w.Result()

fmt.Println(resp.StatusCode)

Полный пример

Давайте теперь построим полный пример. Сначала создайте новый проект Go и создайте файл с именем server.go:

package main

import (
   "fmt"
   "log"
   "net/http"
   "net/url"
)

func RequestHandler(w http.ResponseWriter, r *http.Request) {
   query, err := url.ParseQuery(r.URL.RawQuery)
   if err != nil {     w.WriteHeader(http.StatusBadRequest)
       fmt.Fprintf(w, "Bad request")
       return
   }
   name := query.Get("name")
   if len(name) == 0 {     w.WriteHeader(http.StatusBadRequest)
       fmt.Fprintf(w, "You must supply a name")
       return
   }
   w.WriteHeader(http.StatusOK)
   fmt.Fprintf(w, "Hello %s", name)
}
func main() {
   http.HandleFunc("/greet", RequestHandler)  
log.Fatal(http.ListenAndServe(":3030", nil))
}

Приведенный выше код создает обработчик, который возвращает приветственное сообщение. Параметр запроса имени используется для создания приветствия, которое возвращается в качестве ответа. Запустите код и отправьте запрос на адрес http://localhost:3030/greet с параметром имени, например http://localhost:3030/greet?name=john, чтобы увидеть ответ.

Тестируем наш пример

Чтобы протестировать этот сервер, создайте файл server_test.go и добавьте следующий код:

package main

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

func TestRequestHandler(t *testing.T) {
       expected := "Hello john"

   req := httptest.NewRequest(http.MethodGet, "/greet?name=john", nil)

   w := httptest.NewRecorder()

   RequestHandler(w, req)

   res := w.Result()

   defer res.Body.Close()

   data, err := ioutil.ReadAll(res.Body)

   if err != nil {
       t.Errorf("Error: %v", err)
   }

   if string(data) != expected {
       t.Errorf("Expected Hello john but got %v", string(data))
   }
}

Ожидаемая переменная содержит ожидаемый ответ сервера. Метод NewRequest создает ложный запрос к /greet с параметром имени. Затем обработчик отвечает соответствующими данными, которые затем сверяются с ожидаемым значением. Запустите тест с помощью go test, и вы увидите, что он пройден.

Тестируйте HTTP-клиенты Golang с помощью httptest

Еще один важный вариант использования httptest — тестирование HTTP-клиентов. В то время как HTTP-серверы принимают запросы и выдают ответы, HTTP-клиенты находятся на другом конце, отправляя запросы HTTP-серверу и принимая от него ответы. Тестировать клиентский код сложнее, поскольку он зависит от внешнего HTTP-сервера. Представьте себе сценарий, в котором ваш клиент отправляет запрос к сторонней службе, и вы хотите проверить свой клиент на соответствие всем типам ответов, возвращаемых сторонней службой; однако вы не можете решать, как отреагирует сторонняя служба. Здесь в игру вступает функция NewServer httptest.

Тестирование клиентов с помощью NewServer

Метод NewServer создает новый сервер, который имитирует нужный вам ответ. Вы можете использовать его для имитации ответа сторонней системы для сквозных HTTP-тестов. Давайте посмотрим пример в действии.

Установка зависимостей

Создайте новый проект Go и установите необходимые зависимости:

mkdir httptest-client && cd httptest-client

go init example/user/httptest

go get github.com/pkg/errors

Создайте файл с именем client.go. Здесь вы определите клиента:

package main

import (
   "io/ioutil"
   "net/http"
   "fmt"
   "github.com/pkg/errors"
)

type Client struct {
   url string
}

func NewClient(url string) Client {
   return Client{url}
}

func (c Client) MakeRequest() (string, error) {
   res, err := http.Get(c.url + "/users")
   if err != nil {
       return "", errors.Wrap(err, "An error occured while making the request")

   }

   defer res.Body.Close()

   out, err := ioutil.ReadAll(res.Body)

   if err != nil {

       return "", errors.Wrap(err, "An error occured when reading the response")

   }

   return string(out), nil

}

func main() {

       client := NewClient("https://gorest.co.in/public/v2/")

       resp, err := client.MakeRequest()
       if err != nil {
               panic(err)
       }
       fmt.Println(resp)
}

Клиент просто выполняет вызов API https://gorest.co.in/public/v2/users , который возвращает некоторые данные. Затем ответ выводится на консоль.

Чтобы протестировать клиент, создайте файл client_test.go:

package main

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

func TestClientUpperCase(t *testing.T) {

   // Dummy JSON response
   expected := "{'data': 'dummy'}"
   svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
       fmt.Fprintf(w, expected)
   }))
   defer svr.Close()
   c := NewClient(svr.URL)
   res, err := c.MakeRequest()
   if err != nil {
       t.Errorf("expected err to be nil got %v", err)
   }
   res = strings.TrimSpace(res)
   if res != expected {
       t.Errorf("expected res to be %s got %s", expected, res)
   }
}

Объяснение

Здесь ожидаемая переменная содержит ожидаемый результат, который должен вернуть клиент. В этом случае клиент возвращает все, что он получает от нового сервера, в целости и сохранности, но перед возвратом данных вам может потребоваться некоторая обработка.

Следующая строка создает макет сервера с обработчиком, который возвращает ожидаемый результат:

svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
       fmt.Fprintf(w, expected)
}))

Здесь вы можете изменить значение ответа HTTP, чтобы проверить клиент на различные ответы. Не забудьте также изменить ожидаемое значение.

Наконец, для этого макетного сервера создается клиент и выполняется запрос:

c := NewClient(svr.URL)

res, err := c.MakeRequest()

Запустите тест с помощью go test, чтобы увидеть результаты.

Заключение

Модульные тесты имеют решающее значение для проверки правильности работы любого приложения. Тестирование HTTP-серверов или клиентов затруднено из-за зависимости от внешних сервисов. Чтобы протестировать веб-сервер, вам нужен клиент, и наоборот. Пакет httptest решает эту проблему, имитируя запросы и ответы. Легкий и быстрый характер httptest делает его простым способом тестирования кода Golang с помощью сквозных HTTP-тестов. В этой статье показано, как использовать httptest для тестирования обработчиков API и HTTP-клиентов.