Skip to content

Commit

Permalink
Create assert_response_schema test helper
Browse files Browse the repository at this point in the history
It is a common pattern to use JSON Schema to validate a API response[1], [2]
and [3].

This patch creates the `assert_response_schema` test helper that helps people do
this kind of validation easily on the controller tests.

[1]: https://robots.thoughtbot.com/validating-json-schemas-with-an-rspec-matcher
[2]: https://github.com/sharethrough/json-schema-rspec
[3]: rails-api#1011 (comment)
  • Loading branch information
maurogeorge committed Jan 13, 2016
1 parent 7d4f0c5 commit cb3afa9
Show file tree
Hide file tree
Showing 17 changed files with 549 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Features:
CollectionSerializer for clarity, add ActiveModelSerializers.config.collection_serializer (@bf4)
- [#1295](https://github.com/rails-api/active_model_serializers/pull/1295) Add config `serializer_lookup_enabled` that,
when disabled, requires serializers to explicitly specified. (@trek)
- [#1270](https://github.com/rails-api/active_model_serializers/pull/1270) Adds `assert_response_schema` test helper (@maurogeorge)

Fixes:
- [#1239](https://github.com/rails-api/active_model_serializers/pull/1239) Fix duplicates in JSON API compound documents (@beauby)
Expand Down
1 change: 1 addition & 0 deletions active_model_serializers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'timecop', '~> 0.7'
spec.add_development_dependency 'minitest-reporters'
spec.add_development_dependency 'grape', ['>= 0.13', '< 1.0']
spec.add_development_dependency 'json_schema'
end
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This is the documentation of ActiveModelSerializers, it's focused on the **0.10.
- [How to add root key](howto/add_root_key.md)
- [How to add pagination links](howto/add_pagination_links.md)
- [Using ActiveModelSerializers Outside Of Controllers](howto/outside_controller_use.md)
- [Testing ActiveModelSerializers](howto/test.md)

## Integrations

Expand Down
132 changes: 132 additions & 0 deletions docs/howto/test.md
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"
}
}
}
```
1 change: 1 addition & 0 deletions lib/active_model/serializer/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def config.array_serializer

config.adapter = :attributes
config.jsonapi_resource_type = :plural
config.schema_path = 'test/support/schemas'
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/active_model/serializer/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
end
end
end
1 change: 1 addition & 0 deletions lib/active_model_serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def self.config
autoload :Model
autoload :Callbacks
autoload :Logging
autoload :Test
end

require 'active_model/serializer'
Expand Down
6 changes: 6 additions & 0 deletions lib/active_model_serializers/test.rb
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
103 changes: 103 additions & 0 deletions lib/active_model_serializers/test/schema.rb
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)
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
Loading

0 comments on commit cb3afa9

Please sign in to comment.