Статьи

Проверка JSON Комитетом

Прдч

В моем предыдущем посте я описал JSON-схему, описывающую JSON, который мой сервис примет и вернет. Процесс опирается на гем prmd для предоставления исходных шаблонов схемы JSON, а также для проверки и генерации документов. В общем, это процесс, который кажется, что он должен быть более автоматизированным. Я приложил немало усилий, чтобы создать схему, определяя типы и ссылки вручную в своем текстовом редакторе. В конце работы у меня есть файл .json который описывает, что мой API будет принимать и возвращать, а также соответствует документации Markdown для загрузки.

В этом посте я дал обещание чего-то лучшего. Земля, где возможно написание модульных тестов, которые проверяют JSON, возвращенный моим Rails API. Земля, где производственное приложение будет отклонять запросы, которые не соответствуют нашей схеме JSON. Это звучит как чудесное место? Это для меня.

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

Приложение Счета

Я использую то же приложение, что и сообщение prmd. Это приложение Rails API, предоставляющее информацию об аккаунте. Это не полное приложение, поскольку примеры здесь не требуют этого. Вы можете просмотреть приложение, чтобы увидеть полную схему. Для сегодняшних целей нам просто нужно:

  • тесты RSpec для проверки схемы JSON.
  • запросы к приложению для проверки в соответствии со схемой JSON.

Настроить

Проверьте Gemfile для драгоценного камня, который я использую. Очевидно, что самым важным является жемчужина комитета.

Тесты / функции

Хорошо, до налога на латунь. Я хочу убедиться, что AccountsController возвращает соответствующие ответы на основе моей схемы JSON. Давайте посмотрим на простейшую link в схеме:

 # In the "links" section of schema/schemata/account.json { "description": "Info for existing account.", "href": "/accounts/{(%2Fschemata%2Faccount%23%2Fdefinitions%2Fidentity)}", "method": "GET", "rel": "self", "title": "Info", "targetSchema": { "type": [ "object" ], "properties": { "data": { "type": [ "object" ], "properties": { "id": { "$ref": "#/definitions/account/definitions/id" }, "email": { "$ref": "#/definitions/account/definitions/email" } } } }, "required": [ "data" ] } } 

Путь /account/#{account.id} представляет собой GET с ответом, который выглядит следующим образом:

 { "data": { "id": 1234, "email": "someone@somewhere.com" } } 

В настоящее время AccountController#show выглядит так:

 def show render json: current_account end 

(Примечание: current_account — это простой помощник, который находит учетную запись на основе params[:id] )

Вот начало спецификации:

 describe "GET #show" do # Using FactoryGirl let(:account) { create(:account, email: email, password: password) } let(:email) { Faker::Internet.email } let(:password) { Faker::Internet.password } it "conforms to schema" do get :show, id: account.id # HOW??? end end 

Как мы пишем тест, который проверяет ответ? Вот как сейчас выглядит ответ на /accounts/#{account_id} :

 { "id":3, "email":"adriana@rodriguez.net", "encrypted_password":"$2a$10$D6TkSSCRU4WHwVUzGsV.BeEoIRoBuJEZkuWCj.Ys4PlNJzvmoulzm", "password_salt":"$2a$10$D6TkSSCRU4WHwVUzGsV.Be","jti":null, "reset_password_token":null, "refresh_token":null, "reset_password_sent_at":null, "created_at":"2015-08-16T14:21:50.061Z", "updated_at":"2015-08-16T14:21:50.061Z" } 

Понятно, что это не то, что мы хотим. К счастью, гем комитета предоставляет методы тестирования в Committee::Test::Methods , который необходимо include в спецификацию:

 RSpec.describe AccountsController, type: :controller do include Committee::Test::Methods ... 

Метод, который нас больше всего интересует для этой спецификации, называется assert_schema_conform . Для того, чтобы заставить это совпадение работать в Rails, schema_path last_request schema_path last_request , поскольку метод ожидает schema_path , last_request и last_response . Синтаксис RSpec let делает это простым:

 describe "GET #show" do # Using FactoryGirl let(:account) { create(:account, email: email, password: password) } let(:email) { Faker::Internet.email } let(:password) { Faker::Internet.password } let(:schema_path) { "#{Rails.root}/schema/authentication-api.json" } let(:last_response) { response } let(:last_request) { request } it "conforms to schema" do get :show, id: account.id assert_schema_conform end end 

schema_path указывает на файл схемы JSON, созданный с помощью prmd. last_response и last_request — это соглашения из тестирования Синатры.

Запуск specs ( rspec ) приводит к следующему:

 1) AccountsController GET #show when the token is valid conforms to schema Failure/Error: assert_schema_conform Committee::InvalidResponse: Invalid response. #: failed schema #/definitions/account/links/4/targetSchema: "data" wasn't supplied. # /Users/ggoodrich/.rvm/gems/ruby-2.2.2@sp-json-schema/gems/committee-1.9.1/lib/committee/response_validator.rb:37:in `call' # /Users/ggoodrich/.rvm/gems/ruby-2.2.2@sp-json-schema/gems/committee-1.9.1/lib/committee/test/methods.rb:23:in `assert_schema_conform' # ./spec/controllers/accounts_controller_spec.rb:40:in `block (4 levels) in <top (required)>' 

БУМ! Это то, что мы хотим. Спецификация жалуется, что ответ не включает в себя data . Давайте изменим способ сериализации аккаунта:

 # app/controllers/accounts_controller.rb ... def show account_props = { data: { id: current_account.id, email: current_account.email } } render json: account_props end ... 

Это будет отлично работать, я уверен в этом:

  1) AccountsController GET #show when the token is valid conforms to schema Failure/Error: assert_schema_conform Committee::InvalidResponse: Invalid response. #/data/id: failed schema #/definitions/account/links/4/targetSchema/properties/data/properties/id: 4 is not a string. ... 

Что за что ??? OOOOH riiiight! Я забыл, что я определил идентификатор учетной записи как UUID, который представляет собой строку:

 ... "id": { "description": "unique identifier of account", "readOnly": true, "format": "uuid", "type": [ "string" ] }, ... 

Мы используем UUID для идентификатора учетной записи, но я не реализовал его в приложении таким образом. Поскольку схема JSON была рассмотрена и согласована группой, она является правильной и «источником правды». У нас была пара ситуаций в наших усилиях, когда согласованная схема НЕ была тем, что возвращала реализация. Это, вероятно, сэкономило нам пару раундов oops-fix-deploy , что стоило всех усилий. Кроме того, это побудило команду поддерживать синхронизацию схемы. </moraleOfStory>

Возвращаясь к нашему тесту, после прохождения шагов, чтобы сделать id UUID (это сложнее, чем должно быть для SQLite3, кстати), тесты проходят. Проверьте репо для шагов UUID.

Комитет также проверит, что путь запроса находится в links схемы. Например, если я изменю путь к /account/{id} записи GET схемы JSON на /account/{id} (я удалил ‘s’) и перезапущу спецификации, результат будет следующим:

 1) AccountsController GET #show when the token is valid conforms to schema Failure/Error: assert_schema_conform Committee::InvalidResponse: `GET /accounts/c0adbb04-7706-4da8-8c04-d82079b72287` undefined in schema. ... 

Отлично. Комитет проверит действительные ссылки (путь и метод) и формат ответа. Теперь мы осознали одно из основных преимуществ, которые предлагает Комитет.

Подтверждение реального запроса / ответа

Еще один элемент, поставляемый Комитетом, — это промежуточное программное обеспечение, которое проверяет входящие запросы на соответствие схеме JSON. Committee::Middleware::RequestValidation программное обеспечение поставляется в двух вариантах: Committee::Middleware::RequestValidation и Committee::Middleware::ResponseValidation . Как и другие промежуточные программы, они добавляются в файлы конфигурации Rails:

 #config/application.rb module SpJsonSchemaRails class Application < Rails::Application ... schema_file = "#{Rails.root}/schema/authentication-api.json" if File.exists?(schema_file) config.middleware.use Committee::Middleware::RequestValidation, schema: JSON.parse(File.read(schema_file)), strict: true config.middleware.use Committee::Middleware::ResponseValidation, schema: JSON.parse(File.read(schema_file)) end ... end end 

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

Запрос проверки

Committee::Middleware::RequestValidation принимает следующие параметры (прямо из README ):

  • allow_form_params : Указывает, что альтернативно ввод можно указать как параметры application / x-www-form-urlencoded, если это возможно. Это не будет работать для более сложных проверок схемы.
  • allow_query_params : указывает, что параметры строки запроса будут учитываться при выполнении проверки (по умолчанию true).
  • check_content_type : указывает, что content_type должен быть проверен в соответствии с определением jsonschema. (по умолчанию true).
  • error_class : указывает класс, который будет использоваться для форматирования и вывода ошибок проверки (по умолчанию — Committee :: ValidationError).
  • optimistic_json : попытается проанализировать JSON в теле запроса даже без Content-Type: application / json, прежде чем вернуться к другим параметрам (по умолчанию false).
  • prefix : монтирует промежуточное программное обеспечение для ответа по настроенному префиксу.
  • raise : raise исключение при ошибке, вместо того, чтобы отвечать общим телом ошибки (по умолчанию false).
  • strict : переводит промежуточное ПО в строгий режим, что означает, что пути, которые не определены в схеме, будут обрабатываться 404 вместо запуска (по умолчанию false).

Давайте используем curl в нашем приложении, чтобы увидеть, как это работает:

 # This path doesn't exist and we're using 'strict: true' curl http://localhost:3000/account => {"id":"not_found","message":"That request method and path combination isn't defined."} # This one should work, right? 🙂 curl http://localhost:3000/accounts/953cff00-23ac-47dc-bb68-4b391e75aae7 ==> {"id":"invalid_response","message":"Invalid response.\n\n#/data/id: failed schema #/definitions/account/links/4/targetSchema/properties/data/properties/id: 953CFF0023AC47DCBB684B391E75AAE7 is not a valid uuid."} 

Вау! Что это? Похоже, у нас есть несоответствие между тем, что возвращают спецификации, и тем, что фактически возвращает приложение. Хммм. Что ж, давайте воспользуемся этим, как шанс указать на ответную реакцию при кашле .

Хорошо, одно маленькое исправление, и оно будет работать:

 # app/controllers/accounts_controller.rb ... def show account_props = { data: { id: current_account.id.to_s, # to_s makes the UUID not BELIKETHIS email: current_account.email } } render json: account_props end . curl http://localhost:3000/accounts/953cff00-23ac-47dc-bb68-4b391e75aae7 => {"data":{"id":"953cff00-23ac-47dc-bb68-4b391e75aae7","email":"email@email.com"}} 

В реальном приложении вы должны использовать платформу сериализации, такую ​​как active_model_serializers или roar , так что вы, вероятно, избежите этой проблемы.

Здесь важно отметить, что неверный запрос не попадет на ваш контроллер. Это называется быстро проваливаться, и все крутые дети делают это.

Проверка ответа

Committee::Middleware::ResponseValidation имеет следующие параметры (опять же, прямо из README):

  • error_class : указывает класс, который будет использоваться для форматирования и вывода ошибок проверки (по умолчанию — Committee::ValidationError ).
  • prefix : монтирует промежуточное программное обеспечение для ответа по настроенному префиксу.
  • raise : raise исключение при ошибке, вместо того, чтобы отвечать общим телом ошибки (по умолчанию false).
  • validate_errors : также проверять ответы не-2xx (по умолчанию false)

Для меня наиболее интересным вариантом здесь является error_class . Это позволяет вам указать класс, который будет использоваться для возврата ошибок. Мы действительно хотели эту опцию, чтобы наши ошибки могли соответствовать JSON API .

Мы уже видели пример работы по проверке ответов, поэтому давайте перейдем к тому, что еще Комитет приносит партии.

Заглушка сервера

Одна очень крутая особенность комитета — возможность отключить сервер на основе схемы JSON. Если мы добавим следующую строку в config / application.rb :

 ...other config... use Committee::Middleware::Stub, schema: JSON.parse(File.read("#{Rails.root/schema/authentication-api.json}")) 

и запустив сервер, мы можем нажать любую из ссылок, определенных в нашей схеме JSON, без необходимости их реализации. Это безумно круто.

Например, мы не коснулись POST /account/session которая является ссылкой «Войти». Схема JSON принимает параметр account с email и password и возвращает token :

 { "description": "Sign in (generate token)", "href": "/account/session", "method": "POST", "rel": "", "schema": { "properties": { "account": { "type": [ "object" ], "properties": { "email": { "$ref": "#/definitions/account/definitions/email" }, "password": { "type": [ "string" ], "description": "The password" }, "remember_me": { "type": [ "boolean" ], "description": "True/false - generate refresh token (optional)" } } } }, "type": [ "object" ], "required": [ "account" ] }, "targetSchema": { "type": [ "object" ], "properties": { "token": { "$ref": "#/definitions/account/definitions/token" } } }, "title": "Sign In" }, ... 

Я могу выполнить запрос curl и получить ожидаемый результат:

 curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST http://localhost:3000/account/session -d '{"account": {"email": "test@test.com", "password":"password"}}' HTTP/1.1 200 OK Content-Type: application/json Etag: W/"d36cc07cb95b45bc67964243f0c35795" Cache-Control: max-age=0, private, must-revalidate X-Request-Id: a86b8f12-6d83-4eb8-a750-01331e802243 X-Runtime: 0.002147 Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13) Date: Sun, 16 Aug 2015 20:47:15 GMT Content-Length: 546 Connection: Keep-Alive {"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJkYXRhIjp7ImlkIjoiMTE0MzYiLCJ0eXBlIjoiYWNjb3VudHMiLCJhdHRyaWJ1dGVzIjp7ImVtYWlsIjoiZ2xlbm4uZ29vZHJpY2hAZ21haWwuY29tIn19LCJzdWIiOiJhY2NvdW50IiwiZXhwIjoxNDM3MjM0OTM0LCJpc3MiOiJVbmlxdWUgVVNBIiwiaWF0IjoxNDM3MTQ4NTM0LCJqdGkiOiI3ZmJiYTgzOS1kMGRiLTQwODItOTBmZC1kNmMwM2YwN2NmMWMifQ.SuAAhWPz_7VfJ2iyQpPEHjAnj_aZ-0-gI4uptFucWWflQnrYJl3Z17vAjypiQB_6io85Nuw7VK0Kz2_VHc7VHZwAjxMpzSvigzpUS4HHjSsDil8iYocVEFlnJWERooCOCjSB9R150Pje1DKB8fNeePUGbkCDH6QSk2BsBzT07yT-7zrTJ7kRlsJ-3Kw2GDnvSbb_k2ecX_rkeMeaMj3FmF3PDBNlkM"} 

Ответ основан на `examples`, определяемой для каждого поля в файле схемы JSON.

Committee также предоставляет исполняемый файл committee_stub , который запускает сервер на основе того же файла схемы:

 committee-stub -p 3000 schema/authentication-api.json 

И, БАНГО, рабочий сервер API. Как это круто?

Очевидным примером использования здесь является возможность предоставить «рабочую» реализацию сервера другим командам или дизайнерам, пока вы работаете над реализацией.

Присоединяйтесь к Комитету

Gem комитета предоставляет некоторые крайне необходимые функции для любой команды, создающей серверы API в Ruby. В то время как в сегодняшних примерах используется Rails, для pliny были созданы как prmd , так и gem комитета, который представляет собой каркас на основе Sinatra. Если вы создаете API, вам нужно начать обрабатывать JSON, как будто он является взрослой частью приложения. PRMD и Комитет дают вам инструменты для этого.

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