Разделы портала

Онлайн-тренинги

.
Введение в тестирование REST API на Go с использованием Resty
07.08.2020 00:00

Автор: Баз Дейкстра (Bas Djikstra)
Оригинал статьи
Перевод: Ольга Алифанова

Около года назад я работал в команде, которая, помимо прочего, отвечала за создание и запуск микросервисов, поставляющих данные фронтэнду. Оказалось, что эти сервисы были написаны на Go, языке программирования, о котором я что-то слышал, но с которым никогда не работал.

Как тестировщик и инженер-автоматизатор, я получил задачу создания автоматизированных проверок, которые можно было бы встроить в создание билда, и поэтому я решил, что неплохо бы получить новые навыки и изучить новые инструменты, создавая проверки на том же самом языке. Go – язык относительно молодой, появившийся в 2009 году. Это означает, что инструментарий Go не настолько развит, как, скажем, у Java или C#. Несмотря на это, я довольно быстро создал и запустил эти тесты.

Недавно я снова наткнулся на Go (не помню, где и когда, но это и не важно) и подумал, что будет хорошей идеей вернуться к этому языку. В этой статье я хочу показать, как писать и запускать тесты REST API на Go. API, которым я буду пользоваться – это вновь Zippopotam.us API.

У Go есть встроенная поддержка тестирования – в отличие от языков вроде Java или C#, вам не придется добавлять фреймворк юнит-тестирования в ваш проект. Все, что вам нужно – это импортировать библиотеку testing в код, назвать Go-файл так, чтобы он заканчивался на _test.go, создать метод с именем, начинающимся с Test, запустить

go test

и вы готовы к работе. Для настройки запросов, перехватов, интерпретации и проверки ответов мне очень пригодилась библиотека Resty. Она довольно похожа на Python-библиотеку requests или C#-библиотеку RestSharp. Вот как на Go с Resty выглядит первый тест – проверка кода статуса ответа для GET-запросов к API в правильном формате:

  1. func Test_GetUs90210_StatusCodeShouldEqual200(t *testing.T) {
  2. client := resty.New()
  3. resp, _ := client.R().Get("http://api.zippopotam.us/us/90210")
  4. if resp.StatusCode() != 200 {
  5. t.Errorf("Unexpected status code, expected %d, got %d instead", 200, resp.StatusCode())
  6. }
  7. }

Обратите внимание, что я не собираюсь объяснять все тонкости Go и его синтаксиса. Если вы хотите узнать больше, я рекомендую эту книгу или тур по Go.

Если запустить этот тест командой go test, мы увидим, что он пройден.

Каждый тестовый метод берет аргумент типа T (из библиотеки testing), который используется для управления тест-состоянием. Вам может, как мне, показаться странным, что в этом тесте нет ассерта, которого ожидаешь, если знаком с тест-фреймворками вроде JUnit, NUnit или pytest. Причина проста: их не существует в Go-библиотеке testing. У людей, создавших Go, есть на то свои причины, но мне это не особенно нравится. Я предпочитаю использовать ассерты, потому что они делают код более читабельным, а также избавляют меня от самостоятельного создания всех этих if-then-else-конструкций.

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

  1. func Test_GetUs90210_StatusCodeShouldEqual200(t *testing.T) {
  2. client := resty.New()
  3. resp, _ := client.R().Get("http://api.zippopotam.us/us/90210")
  4. assert.Equal(t, 200, resp.StatusCode())
  5. }

По-моему, намного лучше. Давайте посмотрим, можем ли мы проверить значение заголовка ответа:

  1. func Test_GetUs90210_ContentTypeShouldEqualApplicationJson(t *testing.T) {
  2. client := resty.New()
  3. resp, _ := client.R().Get("http://api.zippopotam.us/us/90210")
  4. assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
  5. }

Это довольно прямолинейно. Теперь как насчет извлечения и проверки значения тела ответа? Самый простой способ сделать это в Go – это демаршалировать (преобразовать) тело ответа в struct (структура данных в Go). Чтобы сделать это, для начала нужно определить struct вот так:

  1. type LocationResponse struct {
  2. Country string `json:"country"`
  3. }

Это определяет struct LocationResponse с единственным элементом Country. Тэг json:”country” говорит Go, что в этот элемент должно попадать значение элемента country из JSON-тела ответа. О других элементах в этом ответе не стоит волноваться – они просто не попадут в структуру (если вам нужно проверить и их, то придется добавить их в struct).

Теперь мы можем создать тест, который преобразует тело в struct типа LocationResponse, а затем проверить значение элемента Country:

  1. func Test_GetUs90210_CountryShouldEqualUnitedStates(t *testing.T) {
  2. client := resty.New()
  3. resp, _ := client.R().Get("http://api.zippopotam.us/us/90210")
  4. myResponse := LocationResponse{}
  5. err := json.Unmarshal(resp.Body(), &myResponse)
  6. if err != nil {
  7. fmt.Println(err)
  8. return
  9. }
  10. assert.Equal(t, "United States", myResponse.Country)
  11. }

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

  1. type ZippopotamUsTestSuite struct {
  2. suite.Suite
  3. ApiClient *resty.Client
  4. }
  5. func (suite *ZippopotamUsTestSuite) SetupTest() {
  6. suite.ApiClient = resty.New()
  7. }
  8. func (suite *ZippopotamUsTestSuite) Test_GetUs90210_StatusCodeShouldEqual200() {
  9. resp, _ := suite.ApiClient.R().Get("http://api.zippopotam.us/us/90210")
  10. assert.Equal(suite.T(), 200, resp.StatusCode())
  11. }

Вначале мы создаем struct ZippopotamUsTestSuite, содержащий все общие для всех тестов объекты. В этом случае все, что мне нужно – это Client (класс в Resty) по имени ApiClient (в Go имя переменной идет перед типом данных при определении новой переменной). Затем мы можем написать метод SetupTest() для настройки всех наших тестов, и использовать (suite *ZippopotamUsTestSuite), чтобы существующие тест-методы стали частью определенного нами набора.

Для запуска нашего тест-набора нужно создать "обычный" тест-метод и передать созданный набор в метод Testify suite.Run():

  1. func TestZippopotamUsSuite(t *testing.T) {
  2. suite.Run(t, new(ZippopotamUsTestSuite))
  3. }

Если вы пропустите этот шаг, go test не запустит тест-набор!

В целом, учитывая, что я в основном работаю с Java, C# и Python, я считаю, что создание тестов в Go более громоздко, нежели в этих языках. Однако при правильном наборе инструментов вполне возможно писать читабельные и хорошо структурированные Go-тесты, как (я надеюсь) демонстрируют примеры в статье.

Я решил исследовать создание тестов (и другого ПО) на Go далее, поэтому начал учить этот язык на этих курсах от Coursera. Они в основном направлены на тех, кто хочет стать Go-разработчиком, и о тестировании там почти не говорят, но я все равно планирую продолжать. Постараюсь делиться советами и тонкостями создания Go-тестов в будущем!

Если вы хотите узнать больше о тестировании на Go, то рекомендую статью Алекса Эллиса и обучающий материал.

Весь код из статьи можно найти здесь.

Обсудить в форуме