Пишем API gem: как написать тесты для внешнего API
В прошлой статье мы разобрались как совладать с Hypermedia API и зачем оно вообще нужно. С момента написания той статьи я успел добавить все API точки в код гема, а так же внёс некоторые мелкие фиксы. Вот ссылка на все изменения с того момента: 8056904...c9088e6.
В этой статье мы исправим главный недостаток гема GrooveHQ: отсутствие тестов. Тестирование кода, зависящего от внешнего API – неприятная, но часто встречающаяся задача. Я постараюсь упростить её как можно сильней.
Если тебя мучает вопрос “почему не TDD? А как же red green refactor?”, то прочитай статью про написание первых тестов платформы mkdev, там я объясняю причины моей не любви к этому подходу.
Настройка тестового окружения
В качестве библиотеки для тестов я снова выбираю Rspec – особых преимуществ кроме привычности и бо`льшей популярности я не вижу, но и их достаточно чтобы сделать выбор. Обновим Gemfile:
# ./Gemfile
# ...
group :test do
gem 'rspec'
end
Внимательный читатель спросит: а почему я добавил гем в Gemfile, а не в gemspec? Отвечаю: просмотрев исходники популярных гемов и пару статей я пришёл к выводу, что гемы для тестов не являются настоящей зависимостью гема, а значит их не стоит добавлять в gemspec.
Дальше нужно добавить папку spec
и файл spec/spec_helper.rb
:
# ./spec/spec_helper.rb
require_relative '../lib/groovehq’ # Подгружаем гем
RSpec.configure do |config|
end
Можно переходить к тестам.
Выявление недоработок в коде тестами
Первый же написанный мой тест выловил небольшой косяк в коде:
# ./spec/groovehq/resource_spec.rb
require 'spec_helper'
describe GrooveHQ::Resource do
let(:client) { GrooveHQ::Client.new("phantogram") }
it "returns empty data for invalid input" do
resource = GrooveHQ::Resource.new(client, "")
expect(resource.data).to eql({})
end
end
Оказывается, если в качестве данных передать пустую строчку, то старая версия кода выдаст исключение. Обновленный код такой проблемой не страдает и позволяет тесту снова стать зелёным:
# ./lib/groovehq/resource.rb
module GrooveHQ
class Resource
attr_reader :rels, :data
def initialize(client, data)
data = {} unless data.is_a?(Hash)
@client = client
@data = data.with_indifferent_access
links = @data.delete(:links)
links[:self] = @data.delete(:href) if @data.has_key?(:href)
@rels = parse_links(links).with_indifferent_access
end
# ...
Воодушевившись первой победой, добавляю ещё несколько тестов для GrooveHQ::Resource
:
# ./spec/groovehq/resource_spec.rb
# ...
it "parses relations correctly" do
data = {
links: {
assignee: {
href: "https://api.groovehq.com/v1/agents/matt@groovehq.com"
}
}
}
resource = GrooveHQ::Resource.new(client, data)
expect(resource.rels[:assignee]).to be_instance_of(GrooveHQ::Relation)
end
it "parses self relation correctly" do
data = {
href: "https://api.groovehq.com/v1/agents/matt@groovehq.com"
}
resource = GrooveHQ::Resource.new(client, data)
expect(resource.rels[:self]).to be_instance_of(GrooveHQ::Relation)
end
it "parses data correctly" do
data = {
name: "When I am small"
}
resource = GrooveHQ::Resource.new(client, data)
expect(resource.data[:name]).to eql "When I am small"
end
# ...
Заодно нашёл ещё один баг, смотри полный коммит со всеми тестами на данный момент: f1b5da4. Немного подумав, я решил разбить тесты этого класса на контексты, где каждый контекст посвящён отдельному методу: ed0cd7a.
Лирическое отступление
Прежде чем я продолжу писать тесты, хочется просто поговорить за жизнь. Во-первых, я решил не покрывать тестами все методы, отвечающие на конкретные API-точки. Например, вот такой метод:
def customer(email)
get("/customers/#{email}")
end
Единственный тест, который я могу здесь придумать – это сделать stub результата HTTP-запроса и сравнить, что вызов этого метода возвращает результат этого stub’а. Получается какой-то странный тест. А таких методов сейчас 23. Засорять папку с тестами такими вот бестолковыми проверками, которые потеряют актуальность после малейшего обновления API смысла нет.
Таким образом, мне остаётся только написать тесты на метод GrooveHQ::Client::Connection#parse_data
и можно считать 80% важного кода покрытым тестами.
Тестируем запросы к API
Сразу же возникла одна концептуальная проблема: метод #parse_data
помечен как private
, а значит я не могу вызывать его напрямую, только при помощи send. Да и в целом, одна из особенностей приватных методов в том, что их не нужно тестировать напрямую.
К сожалению, это означает что нам не обойтись без стабов (stubs), так как все публичные методы, которые я могу вызвать выполняют запрос к API. Значит, нужно “стабать” эти запросы и вместо проведения запроса просто возвращать готовый JSON ответ. Для этой задачи я буду использовать гем WebMock, который подключил в коммите 2bf8b92.
Здесь вместо разбивки на контексты по методам, я сделал контекст для каждого типа ответов от API. Вот, например, тест для самого простого варианта – один корневой ключ со всеми атрибутами ресурса внутри:
# ./spec/groovehq/client/connection_spec.rb
# ...
context "nested hash as response with single root key" do
let(:response) do
{
ticket: {
number: 1,
summary: "This is the first 100 characters of the first message...",
links: {
assignee: {
href: "https://api.groovehq.com/v1/agents/matt@groovehq.com"
}
}
}
}.to_json
end
before(:each) do
stub_request(:get, "https://api.groovehq.com/v1/tickets/1").to_return(body: response)
end
it "returns resource for single root key" do
expect(client.get("/tickets/1")).to be_instance_of(GrooveHQ::Resource)
end
it "returns resource with correct relations" do
expect(client.get("/tickets/1").rels[:assignee].href).to eql("https://api.groovehq.com/v1/agents/matt@groovehq.com")
end
end
# ...
Строчкой stub_request(:get, "https://api.groovehq.com/v1/tickets/1").to_return(body: response)
я говорю: “вместо проведения настоящего запроса по этому адресу просто возвращай мне сразу response
”. Тест получился достаточно объёмным, поэтому за всеми спеками нужно идти и смотреть коммит 69e56b4.
TravisCI
На этом, поидее, можно было бы и завершить написание тестов. Но я посчитал хорошей идеей добавить к проекту Travis CI – самое популярное CI решение для OpenSource проектов. Добавляется он просто – созданием файла .travis.yml и следованием документации.
В результате после каждого коммита прогоняются тесты и все, кто пользуется гемом всегда могут проверить работоспособен ли он. GrooveHQ гем, например, работоспособен.
Что дальше?
На данный момент гем feature complete – можно подключать его к своему приложению и использовать для удобного обращения к любым API точкам GrooveHQ. Все нуждающиеся в этом участки кода покрыты тестами, и у гема даже есть красивый значок от Трэвиса, рапортующий о том, что эти тесты проходят. Впринципе, на этом можно и остановиться. Но вместо этого тебя ждут ещё одна статья, а меня – ещё немного работы по улучшению гема.
Во-первых, в следующей статье мы рассмотрим метапрограммирование в Ruby. Мне хочется использовать что-то подобное: client.ticket(2).state
вместо client.ticket(2).rels[:state].get.data
.
Во-вторых, нужно куда-то этот гем опубликовать, а ещё написать к нему документацию.
Так что впереди по-прежнему много интересного. Если на твой взгляд в геме не хватает чего-то ещё – пиши в комментариях и, возможно, появится необходимость в ещё нескольких статьях ;-)