Skip to content

Commit cb3afa9

Browse files
committed
Create assert_response_schema test helper
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]: #1011 (comment)
1 parent 7d4f0c5 commit cb3afa9

File tree

17 files changed

+549
-0
lines changed

17 files changed

+549
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Features:
4343
CollectionSerializer for clarity, add ActiveModelSerializers.config.collection_serializer (@bf4)
4444
- [#1295](https://github.com/rails-api/active_model_serializers/pull/1295) Add config `serializer_lookup_enabled` that,
4545
when disabled, requires serializers to explicitly specified. (@trek)
46+
- [#1270](https://github.com/rails-api/active_model_serializers/pull/1270) Adds `assert_response_schema` test helper (@maurogeorge)
4647

4748
Fixes:
4849
- [#1239](https://github.com/rails-api/active_model_serializers/pull/1239) Fix duplicates in JSON API compound documents (@beauby)

active_model_serializers.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ Gem::Specification.new do |spec|
5454
spec.add_development_dependency 'timecop', '~> 0.7'
5555
spec.add_development_dependency 'minitest-reporters'
5656
spec.add_development_dependency 'grape', ['>= 0.13', '< 1.0']
57+
spec.add_development_dependency 'json_schema'
5758
end

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This is the documentation of ActiveModelSerializers, it's focused on the **0.10.
2222
- [How to add root key](howto/add_root_key.md)
2323
- [How to add pagination links](howto/add_pagination_links.md)
2424
- [Using ActiveModelSerializers Outside Of Controllers](howto/outside_controller_use.md)
25+
- [Testing ActiveModelSerializers](howto/test.md)
2526

2627
## Integrations
2728

docs/howto/test.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# How to test
2+
3+
## Dependencies
4+
5+
To use the `assert_response_schema` you need to have the
6+
[`json_schema`](https://github.com/brandur/json_schema) on your Gemfile. Please
7+
add it to your Gemfile and run `$ bundle install`.
8+
9+
## Minitest test helpers
10+
11+
ActiveModelSerializers provides a `assert_response_schema` method to be used on your controller tests to
12+
assert the response against a [JSON Schema](http://json-schema.org/). Let's take
13+
a look in an example.
14+
15+
```ruby
16+
class PostsController < ApplicationController
17+
def show
18+
@post = Post.find(params[:id])
19+
20+
render json: @post
21+
end
22+
end
23+
```
24+
25+
To test the `posts#show` response of this controller we need to create a file
26+
named `test/support/schemas/posts/show.json`. The helper uses a naming convention
27+
to locate the file.
28+
29+
This file is a JSON Schema representation of our response.
30+
31+
```json
32+
{
33+
"properties": {
34+
"title" : { "type" : "string" },
35+
"content" : { "type" : "string" }
36+
}
37+
}
38+
```
39+
40+
With all in place we can go to our test and use the helper.
41+
42+
```ruby
43+
class PostsControllerTest < ActionController::TestCase
44+
test "should render right response" do
45+
get :index
46+
assert_response_schema
47+
end
48+
end
49+
```
50+
51+
### Load a custom schema
52+
53+
If we need to use another schema, for example when we have a namespaced API that
54+
shows the same response, we can pass the path of the schema.
55+
56+
```ruby
57+
module V1
58+
class PostsController < ApplicationController
59+
def show
60+
@post = Post.find(params[:id])
61+
62+
render json: @post
63+
end
64+
end
65+
end
66+
```
67+
68+
```ruby
69+
class V1::PostsControllerTest < ActionController::TestCase
70+
test "should render right response" do
71+
get :index
72+
assert_response_schema('posts/show.json')
73+
end
74+
end
75+
```
76+
### Change the schema path
77+
78+
By default all schemas are created at `test/support/schemas`. If we are using
79+
RSpec for example we can change this to `spec/support/schemas` defining the
80+
default schema path in an initializer.
81+
82+
```ruby
83+
ActiveModelSerializers.config.schema_path = 'spec/support/schemas'
84+
```
85+
86+
### Using with the Heroku’s JSON Schema-based tools
87+
88+
To use the test helper with the [prmd](https://github.com/interagent/prmd) and
89+
[committee](https://github.com/interagent/committee).
90+
91+
We need to change the schema path to the recommended by prmd:
92+
93+
```ruby
94+
ActiveModelSerializers.config.schema_path = 'docs/schema/schemata'
95+
```
96+
97+
We also need to structure our schemata according to Heroku's conventions
98+
(e.g. including
99+
[required metadata](https://github.com/interagent/prmd/blob/master/docs/schemata.md#meta-data)
100+
and [links](https://github.com/interagent/prmd/blob/master/docs/schemata.md#links).
101+
102+
### JSON Pointers
103+
104+
If we plan to use [JSON
105+
Pointers](http://spacetelescope.github.io/understanding-json-schema/UnderstandingJSONSchema.pdf) we need to define the `id` attribute on the schema. Example:
106+
107+
```json
108+
# attributes.json
109+
110+
{
111+
"id": "file://attributes.json#",
112+
"properties": {
113+
"name" : { "type" : "string" },
114+
"description" : { "type" : "string" }
115+
}
116+
}
117+
```
118+
119+
```json
120+
# show.json
121+
122+
{
123+
"properties": {
124+
"name": {
125+
"$ref": "file://attributes.json#/properties/name"
126+
},
127+
"description": {
128+
"$ref": "file://attributes.json#/properties/description"
129+
}
130+
}
131+
}
132+
```

lib/active_model/serializer/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def config.array_serializer
2121

2222
config.adapter = :attributes
2323
config.jsonapi_resource_type = :plural
24+
config.schema_path = 'test/support/schemas'
2425
end
2526
end
2627
end

lib/active_model/serializer/railtie.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,9 @@ class Railtie < Rails::Railtie
1919
app.load_generators
2020
require 'generators/serializer/resource_override'
2121
end
22+
23+
if Rails.env.test?
24+
ActionController::TestCase.send(:include, ActiveModelSerializers::Test::Schema)
25+
end
2226
end
2327
end

lib/active_model_serializers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def self.config
1313
autoload :Model
1414
autoload :Callbacks
1515
autoload :Logging
16+
autoload :Test
1617
end
1718

1819
require 'active_model/serializer'

lib/active_model_serializers/test.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module ActiveModelSerializers
2+
module Test
3+
extend ActiveSupport::Autoload
4+
autoload :Schema
5+
end
6+
end
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
module ActiveModelSerializers
2+
module Test
3+
module Schema
4+
# A Minitest Assertion that test the response is valid against a schema.
5+
# @params schema_path [String] a custom schema path
6+
# @params message [String] a custom error message
7+
# @return [Boolean] true when the response is valid
8+
# @return [Minitest::Assertion] when the response is invalid
9+
# @example
10+
# get :index
11+
# assert_response_schema
12+
def assert_response_schema(schema_path = nil, message = nil)
13+
matcher = AssertResponseSchema.new(schema_path, response, message)
14+
assert(matcher.call, matcher.message)
15+
end
16+
17+
MissingSchema = Class.new(Errno::ENOENT)
18+
InvalidSchemaError = Class.new(StandardError)
19+
20+
class AssertResponseSchema
21+
attr_reader :schema_path, :response, :message
22+
23+
def initialize(schema_path, response, message)
24+
require_json_schema!
25+
@response = response
26+
@schema_path = schema_path || schema_path_default
27+
@message = message
28+
@document_store = JsonSchema::DocumentStore.new
29+
add_schema_to_document_store
30+
end
31+
32+
def call
33+
json_schema.expand_references!(store: document_store)
34+
status, errors = json_schema.validate(response_body)
35+
@message ||= errors.map(&:to_s).to_sentence
36+
status
37+
end
38+
39+
protected
40+
41+
attr_reader :document_store
42+
43+
def controller_path
44+
response.request.filtered_parameters[:controller]
45+
end
46+
47+
def action
48+
response.request.filtered_parameters[:action]
49+
end
50+
51+
def schema_directory
52+
ActiveModelSerializers.config.schema_path
53+
end
54+
55+
def schema_full_path
56+
"#{schema_directory}/#{schema_path}"
57+
end
58+
59+
def schema_path_default
60+
"#{controller_path}/#{action}.json"
61+
end
62+
63+
def schema_data
64+
load_json_file(schema_full_path)
65+
end
66+
67+
def response_body
68+
load_json(response.body)
69+
end
70+
71+
def json_schema
72+
@json_schema ||= JsonSchema.parse!(schema_data)
73+
end
74+
75+
def add_schema_to_document_store
76+
Dir.glob("#{schema_directory}/**/*.json").each do |path|
77+
schema_data = load_json_file(path)
78+
extra_schema = JsonSchema.parse!(schema_data)
79+
document_store.add_schema(extra_schema)
80+
end
81+
end
82+
83+
def load_json(json)
84+
JSON.parse(json)
85+
rescue JSON::ParserError => ex
86+
raise InvalidSchemaError, ex.message
87+
end
88+
89+
def load_json_file(path)
90+
load_json(File.read(path))
91+
rescue Errno::ENOENT
92+
raise MissingSchema, "No Schema file at #{schema_full_path}"
93+
end
94+
95+
def require_json_schema!
96+
require 'json_schema'
97+
rescue LoadError
98+
raise LoadError, "You don't have json_schema installed in your application. Please add it to your Gemfile and run bundle install"
99+
end
100+
end
101+
end
102+
end
103+
end

0 commit comments

Comments
 (0)