Хабрахабр

Gonkey — инструмент тестирования микросервисов

Если функциональность ваших сервисов реализована преимущественно через API, и используется JSON для обмена данными, то почти наверняка Gonkey подойдет и вам. Gonkey тестирует наши микросервисы в Lamoda, и мы подумали, что он может протестировать и ваши, поэтому выложили его в open source.

image

Ниже я расскажу о нем подробнее и покажу на конкретных примерах, как его использовать.

Как родился Gonkey

Все сервисы имеют API. У нас более ста микросервисов, каждый из которых решает какую-то свою задачу. Конечно, некоторые из них — еще и пользовательский интерфейс, но, все же, первостепенная роль у них — это быть источником данных для сайта, мобильных приложений или других внутренних сервисов, а значит предоставлять программный интерфейс.

Если интересно узнать об этом подробнее, посмотрите доклад Андрея с Highload++. Когда мы поняли, что сервисов становится много, а дальше их будет еще больше, то разработали внутренний документ, описывающий стандартный подход к проектированию API, и взяли как инструмент описания Swagger (и даже написали утилиты для генерации кода на основе swagger-спецификации).

Вот чего хотелось добиться: Стандартный подход к проектированию API закономерно навел на мысль о стандартном подходе к тестированию.

  1. Тестировать сервисы через API, потому что через него и реализуется почти вся функциональность сервиса
  2. Возможность автоматизировать запуск тестов, чтобы встроить его в наш процесс CI/CD, как говорится, “запускать по кнопке”
  3. Написание тестов должно быть отчуждаемым, то есть, чтобы тесты мог писать не только программист, в идеале — человек, не знакомый с программированием.

Так родился Gonkey.

Итак, что же это?

Сценарии тестов описываются в YAML-файлах. Gonkey — библиотека (для проектов на Golang) и консольная утилита (для проектов на любых языках и технологиях), с помощью которой можно проводить функциональное и регрессионное тестирование сервисов, путем обращения к их API по заранее составленному сценарию.

Попросту говоря, Gonkey умеет:

  • обстреливать ваш сервис HTTP-запросами и следить, чтобы его ответы соответствовали ожидаемым. Он предполагает, что в запросах и ответах используется JSON, но, скорее всего, сработает и на несложных случаях с ответами в другом формате;
  • подготавливать базу данных к тесту, заполнив ее данными из фикстур (тоже задаются в YAML-файлах);
  • имитировать ответы внешних сервисов с помощью моков (эта фича доступна, только если вы подключаете Gonkey как библиотеку);
  • выдавать результат тестирования в консоль или формировать Allure-отчет.

Репозиторий проекта
Docker-образ

Пример тестирования сервиса с Gonkey

Чтобы не нагружать вас текстом, я хочу перейти от слов к делу и прямо здесь протестировать какой-нибудь API и по ходу дела рассказать и показать, как пишутся сценарии тестов.

Он хранит цвет текущего сигнала: красный, желтый или зеленый. Давайте набросаем маленький сервис на Go, который будет имитировать работу светофора. Получить текущий цвет сигнала или установить новый можно через API.

// возможные состояния светофора
const ( lightRed = "red" lightYellow = "yellow" lightGreen = "green"
) // структура для хранения состояния светофора
type trafficLights struct { currentLight string `json:"currentLight"` mutex sync.RWMutex `json:"-"`
} // экземпляр светофора
var lights = trafficLights{ currentLight: lightRed,
} func main() w.Write(resp) }) // метод для установки нового состояния светофора http.HandleFunc("/light/set", func(w http.ResponseWriter, r *http.Request) { lights.mutex.Lock() defer lights.mutex.Unlock() request, err := ioutil.ReadAll(r.Body) if err != nil { log.Fatal(err) } var newTrafficLights trafficLights if err := json.Unmarshal(request, &newTrafficLights); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := validateRequest(&newTrafficLights); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } lights = newTrafficLights }) // запуск сервера (блокирующий) log.Fatal(http.ListenAndServe(":8080", nil))
} func validateRequest(lights *trafficLights) error { if lights.currentLight != lightRed && lights.currentLight != lightYellow && lights.currentLight != lightGreen { return fmt.Errorf("incorrect current light: %s", lights.currentLight) } return nil
}

Полностью исходный код main.go здесь.

Запустим программу:

go run .

Наверняка где-нибудь ошибся, поэтому напишем тест и проверим. Набросал очень быстро, за 15 минут!

Скачаем и запустим Gonkey:

mkdir -p tests/cases
docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080

Эта команда запускает образ с gonkey через докер, монтирует директорию tests/cases внутрь контейнера и запускает gonkey с параметрами -tests tests/cases/ -host.

Если вам не нравится подход с докером, то альтернативой такой команде было бы написать:

go get github.com/lamoda/gonkey
go run github.com/lamoda/gonkey -tests tests/cases -host localhost:8080

Запустили и получили результат:

Failed tests: 0/0

Напишем первый тест. Нет тестов — нечего проверять. Создадим файл tests/cases/light_get.yaml с минимальным содержимым:

- name: WHEN currentLight is requested MUST return red method: GET path: /light/get response: 200: > { "currentLight": "red" }

Это означает, что мы описали один тест-кейс, но в файле их может быть много. На первом уровне — список. Таким образом, один файл — один сценарий. Вместе они составляют тестируемый сценарий. Можно создать сколько угодно файлов со сценариями тестов, если удобно, разложить их по поддиректориям — gonkey считывает все yaml и yml файлы из переданной директории и глубже рекурсивно.

Еще ниже — код ответа (200) и тело ответа, которые мы ожидаем от сервера. Ниже в файле описаны детали запроса, который будет отправлен на сервер: метод, путь.

Полный формат файла описан в README.

Запустим еще раз:

docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080

Результат:

Name: WHEN currentlight is requested MUST return red Request: Method: GET Path: /light/get Query: Body:
<no body> Response: Status: 200 OK Body:
{} Result: ERRORS! Errors: 1) at path $ values do not match: expected: { "currentLight": "red"
} actual: {} Failed tests: 1/1

Ожидалась структура с полем currentLight, а вернулась пустая структура. Ошибка! Первая проблема — это то, что результат был интерпретирован как строка, об этом говорит нам то, что в качестве проблемного места gonkey подсветил весь ответ целиком, без деталиции: Это плохо.

expected: { "currentLight": "red"
}

Исправляем: Причина простая: я забыл написать, чтобы сервис в ответе указывал тип содержимого application/json.

// метод для получения текущего состояния светофора
http.HandleFunc("/light/get", func(w http.ResponseWriter, r *http.Request) { lights.mutex.RLock() defer lights.mutex.RUnlock() resp, err := json.Marshal(lights) if err != nil { log.Fatal(err) } w.Header().Add("Content-Type", "application/json") // <-- добавилось w.Write(resp)
})

Перезапускаем сервис и прогоняем тесты еще раз:

Name: WHEN currentlight is requested MUST return red Request: Method: GET Path: /light/get Query: Body:
<no body> Response: Status: 200 OK Body:
{} Result: ERRORS! Errors: 1) at path $ key is missing: expected: currentLight actual: <missing>

Теперь gonkey распознает структуру, но она по-прежнему неверная: ответ пустой. Отлично, есть прогресс. Причина в том, что я в определении типа использовал неэкспортируемое поле currentLight:

// структура для хранения состояния светофора
type trafficLights struct { currentLight string `json:"currentLight"` mutex sync.RWMutex `json:"-"`
}

Сериализатор JSON его не видит и не может включить его в ответ. В Go поле структуры, названное со строчной буквы считается неэкспортируемым, то есть, недоступным из других пакетов. Исправляем: делаем поле с заглавной буквы, что означает, что оно экспортируемое:

// структура для хранения состояния светофора
type trafficLights struct { СurrentLight string `json:"currentLight"` // <-- изменилось название mutex sync.RWMutex `json:"-"`
}

Снова запускаем тесты. Перезапускаем сервис.

Failed tests: 0/1

Тесты прошли!

Заполним файл tests/cases/light_set.yaml следующим содержимым: Напишем еще один сценарий, который проверит метод set.

- name: WHEN set is requested MUST return no response method: POST path: /light/set request: > { "currentLight": "green" } response: 200: '' - name: WHEN get is requested MUST return green method: GET path: /light/get response: 200: > { "currentLight": "green" }

Первый тест задает новое значения для сигнала светофора, а второй проверяет состояние, чтобы убедиться, что оно поменялось.

Запустим тесты все той же командой:

docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080

Результат:

Failed tests: 0/3

Что было бы, если бы они выполнились наоборот? Успешный результат, но нам повезло, что сценарии выполнились в нужном нам порядке: сначала light_get, а потом light_set. Давайте переименуем:

mv tests/cases/light_set.yaml tests/cases/_light_set.yaml

И запустим заново:

Errors: 1) at path $.currentLight values do not match: expected: red actual: green Failed tests: 1/3

Сначала выполнился set и оставил светофор в состоянии зеленого, поэтому запущенный следом тест get обнаружил ошибку — он ждал красный.

Одним из способов избавится от того, что тест зависит от контекста — это в начале сценария (то есть в начале файла) проинициализировать сервис, что мы в общем-то и делаем в тесте set — сначала задаем известное значение, которое должно произвести известный эффект, а потом проверяем, что эффект возымел действие.

Описание и примеры работы с фикстурами в gonkey я хочу вынести в отдельную статью. Другой способ подготовить контекст выполнения, если сервис использует базу данных — это использовать фикстуры с данными, которые загружаются в базу в начале сценария, тем самым формируя предсказуемое состояние сервиса, которое можно проверять.

Так как в сценарии set мы фактически тестируем и метод light/set, и light/get, то сценарий light_get, который зависим от контекста, нам попросту не нужен. Пока же я предлагаю следующее решение. Я его удаляю, а оставшийся сценарий переименовываю, чтобы название отражало суть.

rm tests/cases/light_get.yaml
mv tests/cases/_light_set.yaml tests/cases/light_set_get.yaml

Или не отправить цвет вовсе? Следующим шагом я хотел бы проверить некоторые негативные сценарии работы с нашим сервисом, например, корректно ли он отработает, если отправить некорректный цвет сигнала?

Создам новый сценарий tests/cases/light_set_get_negative.yaml:

- name: WHEN set is requested MUST return no response method: POST path: /light/set request: > { "currentLight": "green" } response: 200: '' - name: WHEN incorrect color is passed MUST return error method: POST path: /light/set request: > { "currentLight": "blue" } response: 400: > incorrect current light: blue - name: WHEN color is missing MUST return error method: POST path: /light/set request: > {} response: 400: > incorrect current light: - name: WHEN get is requested MUST have color untouched method: GET path: /light/get response: 200: > { "currentLight": "green" }

Он проверяет, что:

  • когда передан неверный цвет, возникает ошибка;
  • когда цвет не передали, возникает ошибка;
  • передача неверного цвета не меняет внутреннее состояние светофора.

Запустим:

Failed tests: 0/6

Все отлично 🙂

Подключаем Gonkey как библиотеку

Таким же образом мы могли бы протестировать любое публичное API, к исходным кодам которого у нас нет доступа — достаточно возможности отправлять запросы и получать ответы. Как вы заметили, мы тестируем API сервиса, совершенно абстрагируясь от языка и технологий, на которых он написан.

Это позволит, не компилируя ничего заранее — ни gonkey, ни сам проект — прогонять тест простым запуском go test. Но для наших собственных приложений, написанных на go, есть более удобный способ запускать gonkey — подключить его к проекту как библиотеку.

При таком подходе мы как будто начинаем писать юнит-тест, а в теле теста делаем следующее:

  • инициализируем веб-сервер точно так же, как это делается при запуске сервиса;
  • запускаем тестовый сервер приложения на localhost и случайном порту;
  • вызываем функцию из библиотеки gonkey, передавая ей адрес тестового сервера и другие параметры. Ниже я это проиллюстрирую.

Ключевой его момент — вынесение создания сервера в отдельную функцию, потому что нам эта функция теперь понадобится в двух местах: собственно при старте сервиса и еще при запуске тестов gonkey. Для этого нашему приложению понадобится небольшой рефакторинг.

Я выношу следующий код в отдельную функцию:

func initServer() { // метод для получения текущего состояния светофора http.HandleFunc("/light/get", func(w http.ResponseWriter, r *http.Request) { // без изменений }) // метод для установки нового состояния светофора http.HandleFunc("/light/set", func(w http.ResponseWriter, r *http.Request) { // без изменений })
}

Функция main тогда будет такой:

func main() { initServer() // запуск сервера (блокирующий) log.Fatal(http.ListenAndServe(":8080", nil))
}

Измененный файл main go полностью.

Я создаю файл func_test.go: Это развязало нам руки, поэтому приступим к написанию теста.

func Test_API(t *testing.T) { initServer() srv := httptest.NewServer(nil) runner.RunWithTesting(t, &runner.RunWithTestingParams{ Server: srv, TestsDir: "tests/cases", })
}

Вот файл func_test.go полностью.

Проверяем: Вот и все!

go test ./...

Результат:

ok github.com/lamoda/gonkey/examples/traffic-lights-demo 0.018s

Если у меня будут и юнит-тесты, и тесты gonkey, они запустятся все вместе — довольно удобно. Тесты прошли.

Формируем отчет Allure

Gonkey умеет записывать результаты прохождения тестов в таком формате. Allure — это формат отчета о тестировании для отображения результатов в наглядном и красивом виде. Активировать Allure очень просто:

docker run -it -v $(pwd)/tests:/tests -w /tests lamoda/gonkey -tests cases/ -host host.docker.internal:8080 -allure

Отчет будет помещен в поддиректорию allure-results текущей рабочей директории (поэтому я указал -w /tests).

При подключении gonkey как библиотеки Allure-отчет активируется установкой дополнительной переменной окружения GONKEY_ALLURE_DIR:

GONKEY_ALLURE_DIR="tests/allure-results" go test ./…

Результаты тестов, записанные в файлы, превращаются в интерактивный отчет командами:

allure generate
allure serve

Как выглядит отчет:
image

Заключение

В следующих статьях я подробнее остановлюсь на использовании фикстур в gonkey и на имитации ответов других сервисов с помощью моков.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть