-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Create assert_response_schema test helper #1270
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# How to test | ||
|
||
## Dependencies | ||
|
||
To use the `assert_response_schema` you need to have the | ||
[`json_schema`](https://github.com/brandur/json_schema) on your Gemfile. Please | ||
add it to your Gemfile and run `$ bundle install`. | ||
|
||
## Minitest test helpers | ||
|
||
ActiveModelSerializers provides a `assert_response_schema` method to be used on your controller tests to | ||
assert the response against a [JSON Schema](http://json-schema.org/). Let's take | ||
a look in an example. | ||
|
||
```ruby | ||
class PostsController < ApplicationController | ||
def show | ||
@post = Post.find(params[:id]) | ||
|
||
render json: @post | ||
end | ||
end | ||
``` | ||
|
||
To test the `posts#show` response of this controller we need to create a file | ||
named `test/support/schemas/posts/show.json`. The helper uses a naming convention | ||
to locate the file. | ||
|
||
This file is a JSON Schema representation of our response. | ||
|
||
```json | ||
{ | ||
"properties": { | ||
"title" : { "type" : "string" }, | ||
"content" : { "type" : "string" } | ||
} | ||
} | ||
``` | ||
|
||
With all in place we can go to our test and use the helper. | ||
|
||
```ruby | ||
class PostsControllerTest < ActionController::TestCase | ||
test "should render right response" do | ||
get :index | ||
assert_response_schema | ||
end | ||
end | ||
``` | ||
|
||
### Load a custom schema | ||
|
||
If we need to use another schema, for example when we have a namespaced API that | ||
shows the same response, we can pass the path of the schema. | ||
|
||
```ruby | ||
module V1 | ||
class PostsController < ApplicationController | ||
def show | ||
@post = Post.find(params[:id]) | ||
|
||
render json: @post | ||
end | ||
end | ||
end | ||
``` | ||
|
||
```ruby | ||
class V1::PostsControllerTest < ActionController::TestCase | ||
test "should render right response" do | ||
get :index | ||
assert_response_schema('posts/show.json') | ||
end | ||
end | ||
``` | ||
### Change the schema path | ||
|
||
By default all schemas are created at `test/support/schemas`. If we are using | ||
RSpec for example we can change this to `spec/support/schemas` defining the | ||
default schema path in an initializer. | ||
|
||
```ruby | ||
ActiveModelSerializers.config.schema_path = 'spec/support/schemas' | ||
``` | ||
|
||
### Using with the Heroku’s JSON Schema-based tools | ||
|
||
To use the test helper with the [prmd](https://github.com/interagent/prmd) and | ||
[committee](https://github.com/interagent/committee). | ||
|
||
We need to change the schema path to the recommended by prmd: | ||
|
||
```ruby | ||
ActiveModelSerializers.config.schema_path = 'docs/schema/schemata' | ||
``` | ||
|
||
We also need to structure our schemata according to Heroku's conventions | ||
(e.g. including | ||
[required metadata](https://github.com/interagent/prmd/blob/master/docs/schemata.md#meta-data) | ||
and [links](https://github.com/interagent/prmd/blob/master/docs/schemata.md#links). | ||
|
||
### JSON Pointers | ||
|
||
If we plan to use [JSON | ||
Pointers](http://spacetelescope.github.io/understanding-json-schema/UnderstandingJSONSchema.pdf) we need to define the `id` attribute on the schema. Example: | ||
|
||
```json | ||
# attributes.json | ||
|
||
{ | ||
"id": "file://attributes.json#", | ||
"properties": { | ||
"name" : { "type" : "string" }, | ||
"description" : { "type" : "string" } | ||
} | ||
} | ||
``` | ||
|
||
```json | ||
# show.json | ||
|
||
{ | ||
"properties": { | ||
"name": { | ||
"$ref": "file://attributes.json#/properties/name" | ||
}, | ||
"description": { | ||
"$ref": "file://attributes.json#/properties/description" | ||
} | ||
} | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,5 +19,9 @@ class Railtie < Rails::Railtie | |
app.load_generators | ||
require 'generators/serializer/resource_override' | ||
end | ||
|
||
if Rails.env.test? | ||
ActionController::TestCase.send(:include, ActiveModelSerializers::Test::Schema) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too bad include is still private in Ruby 2.0 :( |
||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module ActiveModelSerializers | ||
module Test | ||
extend ActiveSupport::Autoload | ||
autoload :Schema | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
module ActiveModelSerializers | ||
module Test | ||
module Schema | ||
# A Minitest Assertion that test the response is valid against a schema. | ||
# @params schema_path [String] a custom schema path | ||
# @params message [String] a custom error message | ||
# @return [Boolean] true when the response is valid | ||
# @return [Minitest::Assertion] when the response is invalid | ||
# @example | ||
# get :index | ||
# assert_response_schema | ||
def assert_response_schema(schema_path = nil, message = nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would you mind adding comments to the effect of what nil schema_path/message mean and what types of data they are? Yardoc or pseudo-yardoc is preferred. # A Minitest Assertion that the response is valid per the schema
# which uses the given error message
# @params schema_path [String] blah nil blah else
# @params message [String] blah nil blah else
# @example
# get :something
# assert_response_schema and the matcher just returns bool? |
||
matcher = AssertResponseSchema.new(schema_path, response, message) | ||
assert(matcher.call, matcher.message) | ||
end | ||
|
||
MissingSchema = Class.new(Errno::ENOENT) | ||
InvalidSchemaError = Class.new(StandardError) | ||
|
||
class AssertResponseSchema | ||
attr_reader :schema_path, :response, :message | ||
|
||
def initialize(schema_path, response, message) | ||
require_json_schema! | ||
@response = response | ||
@schema_path = schema_path || schema_path_default | ||
@message = message | ||
@document_store = JsonSchema::DocumentStore.new | ||
add_schema_to_document_store | ||
end | ||
|
||
def call | ||
json_schema.expand_references!(store: document_store) | ||
status, errors = json_schema.validate(response_body) | ||
@message ||= errors.map(&:to_s).to_sentence | ||
status | ||
end | ||
|
||
protected | ||
|
||
attr_reader :document_store | ||
|
||
def controller_path | ||
response.request.filtered_parameters[:controller] | ||
end | ||
|
||
def action | ||
response.request.filtered_parameters[:action] | ||
end | ||
|
||
def schema_directory | ||
ActiveModelSerializers.config.schema_path | ||
end | ||
|
||
def schema_full_path | ||
"#{schema_directory}/#{schema_path}" | ||
end | ||
|
||
def schema_path_default | ||
"#{controller_path}/#{action}.json" | ||
end | ||
|
||
def schema_data | ||
load_json_file(schema_full_path) | ||
end | ||
|
||
def response_body | ||
load_json(response.body) | ||
end | ||
|
||
def json_schema | ||
@json_schema ||= JsonSchema.parse!(schema_data) | ||
end | ||
|
||
def add_schema_to_document_store | ||
Dir.glob("#{schema_directory}/**/*.json").each do |path| | ||
schema_data = load_json_file(path) | ||
extra_schema = JsonSchema.parse!(schema_data) | ||
document_store.add_schema(extra_schema) | ||
end | ||
end | ||
|
||
def load_json(json) | ||
JSON.parse(json) | ||
rescue JSON::ParserError => ex | ||
raise InvalidSchemaError, ex.message | ||
end | ||
|
||
def load_json_file(path) | ||
load_json(File.read(path)) | ||
rescue Errno::ENOENT | ||
raise MissingSchema, "No Schema file at #{schema_full_path}" | ||
end | ||
|
||
def require_json_schema! | ||
require 'json_schema' | ||
rescue LoadError | ||
raise LoadError, "You don't have json_schema installed in your application. Please add it to your Gemfile and run bundle install" | ||
end | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
invalid json
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😄