![image](https://habrastorage.org/webt/m0/jt/t5/m0jtt5hnq9j99acma7wijes_fqm.png)
Иногда бывает необходимость написать небольшой АПИ сервис, часто в виде прототипа. И часто этот прототип потом так и остаётся в первоначально написанном виде следуя принципу «работает — не трогай». Переписывание даже относительно маленького сервиса сопряжено с возможностью внесения ошибки или случайного незначительно изменения поведения, которое обнаружится далеко не сразу. На помощь тут приходит тестирование по методу черного ящика (функциональное тестирование). Написание тестов является важной частью процесса разработки, а время потраченное на написание тестов может быть гораздо больше, чем реализация тестируемого функционала. Предлагаю рассмотреть метод тестирования, когда тестируемый код (сервис) и авто тесты написаны на разных языках программирование. Данный подход позволяет писать тесты без зависимости от первоначально выбранной технологии, что позволяет достаточно легко «выкинуть» прототип и переписать требуемый функционал на других технологиях. Плюс это демонстрация того, что тесты не обязательно должны быть написаны на том же языке, что и тестируемый сервис.
Для примера возьмём следующую задачу. Написать http API сервис со следующими методами:
- GET /ping — сервис должен всегда отвечать кодом 200 и текстом «OK».
- GET /movies — сервис отдаёт список фильмов, который в свою очередь получает из стороннего сервиса. Поддерживает фильтрацию через query параметр rating, если параметр не задан, использует значение по умолчанию.
Нам понадобится:
- Rspec — фрэймворк для тестирования на Ruby
- Mockserver — для эмуляции ответа от стороннего сервера
- Go + echo — для написания прототипа АПИ сервиса
Внимание: в данном тексте сознательно опущены любые детали по установке, настройке и использовании рассматриваемых инструментов
Rspec в качестве фрэймворка для тестирования выбран так как синтаксис языка ruby позволяет писать достаточно лаконичные тесты с минимум утилитарного кода. MockServer — является очень мощным инструментом для эмуляции ответов сторонних сервисов, главная особенность — умеет запускаться как независимый http API сервис. Если вы используете другой стэк технологий, то почти наверняка сможете найти наиболее удобные для вас аналоги. Данные инструменты взяты исключительно ради примера.
Шаги для установки и настройки ruby, java и golang я пропускаю. Начнём с Rspec. Для удобства желательно установить bundler. Список используемых гемов будет такой:
gem "rspec"
gem "rest-client"
gem "mockserver-client"
Mockserver имеет достаточно удобное REST API и клиенты для Java и JavaScript. Мы же воспользуемся ruby клиентом, на данный момент он уже явно не поддерживается, но базовый функционал доступен. Генерируем скелет приложения через команду
rspec --init
Затем создаём файл /spec/api_spec.rb:
# /spec/api_spec.rb
require 'spec_helper'
require 'rest-client'
require 'mockserver-client'
RSpec.describe "ApiServer" do
let(:api_server_host) { "http://#{ENV.fetch("API_SERVICE_ADDR", '127.0.0.1:8000')}" }
end
Напишем тест для метода /ping (поместим данный участок кода внутри блока RSpec.describe «ApiServer»)
describe "GET /ping" do
before { @response = RestClient.get "#{api_server_host}/ping" }
it do
expect(@response.code).to eql 200
expect(@response.body).to eql 'OK'
end
end
Если сейчас запустить тест (через команду rspec), то он предсказуемо свалится с ошибкой. Напишем реализацию метода.
package main
import (
"net/http"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
e.GET("/ping", ping)
e.Start(":8000")
}
func ping(c echo.Context) error {
return c.String(http.StatusOK, "OK")
}
Скомпилируем и запустим наш АПИ сервис (например через go run). Для упрощения кода будем запускать сервис и тесты вручную. Запускаем вначале АПИ сервис, потом rspec. В этот раз тест должен пройти успешно. Таким образом мы получили простейший не зависимый тест, с помощью которого можно протестировать реализацию данного АПИ метода на любом языке или сервере.
Усложним пример и добавим второй метод — /movies. Добавляем код теста.
GET /movies
describe "GET /movies" do
let(:params) { {} }
before { @response = RestClient.get "#{api_server_host}/movies", {params: params} }
context '?rating=X' do
let(:params) { {rating: 90} }
let(:query_string_parameters) { [parameter('rating', '90')] }
let(:movies_resp_body) { File.read('spec/fixtures/movies_90.json') }
let(:resp_body) { movies_resp_body }
include_examples 'response_ok'
end
describe 'set default filter' do
let(:query_string_parameters) { [parameter('rating', '70')] }
let(:movies_resp_body) { File.read('spec/fixtures/movies.json') }
let(:resp_body) { movies_resp_body }
include_examples 'response_ok'
end
end
По условию задачи список фильмов необходимо получать из стороннего АПИ, для эмуляции ответа в сторонне АПИ используем mock server. Для этого зададим ему тело ответа и условие при котором он будет им отвечать. Сделать это можно следующим образом:
setup mock
include MockServer
include MockServer::Model::DSL
def create_mock_client
MockServer::MockServerClient.new(ENV.fetch("MOCK_SERVER_HOST", 'localhost'), ENV.fetch("MOCK_SERVER_PORT", 1080))
end
let(:query_string_parameters) { [] }
let(:movies_resp_body) { '[]' }
before :each do
@movies_server = create_mock_client
@movies_server.reset
@exp = expectation do |exp|
exp.request do |request|
request.method = 'GET'
request.path = '/movies'
request.headers << header('Accept', 'application/json')
request.query_string_parameters = query_string_parameters
end
exp.response do |response|
response.status_code = 200
response.headers << header('Content-Type', 'application/json; charset=utf-8')
response.body = body(movies_resp_body)
end
end
@movies_server.register(@exp)
end
И реализацию хэндлера в сервисе АПИ:
movies handler
func movies(c echo.Context) error {
rating := c.QueryParam("rating")
if rating == "" {
rating = "70"
}
client := &http.Client{}
req, _ := http.NewRequest("GET", "http://localhost:1080/movies", nil)
req.Header.Add("Accept", `application/json`)
q := req.URL.Query()
q.Add("rating", rating)
req.URL.RawQuery = q.Encode()
if resp, err := client.Do(req); err != nil {
panic(err)
} else {
return c.Stream(http.StatusOK, "application/json", resp.Body)
}
}
Для запуска тестов теперь необходимо уже запускать три процесса: проверяемый сервис, mock server и rspec.
go run main.go
java -jar mockserver-netty-5.3.0-jar-with-dependencies.jar -serverPort 1080
rspec
Автоматизация данного процесса является отдельной задачей.
Стоит ещё обратить внимание на итоговый размер кода сервиса и тестов для него. Покрытие тестами минимального сервиса на 30 строк требует почти в три раза больше строк кода в тестах, с объёмным кодом на установку моков, но без учета автоматизации запуска и фикстур ответов. С одной стороны это порождает вопрос рациональности тестирования, с другой стороны, данное соотношение в целом является стандартным и показывает, что хорошие тесты — это как минимум половина работы. И их независимость от первоначальной выбранной технологии может стать большим плюсом. Однако, нетрудно заметить, что таким образом крайне затруднительно тестировать состояние БД. Одно из возможных решений данной проблемы — добавление приватного АПИ для изменения состояния БД или создания слепков БД (фикстур) для разных ситуаций.
Gist с листингом
Обсуждение, плюсы, минусы и критика — ждём в комментариях