JsonRspecMatchMaker
Write RSpec matchers for JSON api endpoints using a simple data structure. DRY up API expectations, without losing the specificity sacrificed by a schema-based approach to JSON expectations.
Why?
As pointed out by Thoughbot in their blog post Validating JSON Schemas with an RSpec Matcher the naive pattern for writing request specs for a JSON API in rails tends to look something like:
describe "Fetching the current user" do
context "with valid auth token" do
it "returns the current user" do
user = create(:user)
auth_header = { "Auth-Token" => user.auth_token }
get v1_current_user_url, {}, auth_header
current_user = response_body["user"]
expect(response.status).to eq 200
expect(current_user["auth_token"]).to eq user.auth_token
expect(current_user["email"]).to eq user.email
expect(current_user["first_name"]).to eq user.first_name
expect(current_user["last_name"]).to eq user.last_name
expect(current_user["id"]).to eq user.id
expect(current_user["phone_number"]).to eq user.phone_number
end
end
def response_body
JSON.parse(response.body)
end
end
Tedious to write and just as tedious to read.
In that post, they talk about one alternative for making tests around JSON better - JSON Schema. This is an interesting approach, but I'd like to be more specific about my specs than validating that the shape of the data and the types of the values are correct.
However, I do like the way that the JSON schema is written because it is very similar to the JSON that's generated. So that's the goal - enable writing spec matchers similar to a JSON schema definition but with greater specificity about values.
I think this gem accomplishes that. As an example, here is a JSON Schema defintion (also pulled from the Thoughbot blog post)
{
"type": "object",
"required": ["user"],
"properties": {
"user" : {
"type" : "object",
"required" : [
"auth_token",
"email",
"first_name",
"id",
"last_name",
"phone_number"
],
"properties" : {
"auth_token" : { "type" : "string" },
"created_at" : { "type" : "string", "format": "date-time" },
"email" : { "type" : "string" },
"first_name" : { "type" : "string" },
"id" : { "type" : "integer" },
"last_name" : { "type" : "string" },
"phone_number" : { "type" : "string" },
"updated_at" : { "type" : "string", "format": "date-time" }
}
}
}
}
And here is what the interesting bits of a matcher using this gem would look like for the same case:
{
'user.auth_token' => ->(user) { user.auth_token },
'user.created_at' => ->(user) { user.created_at },
'user.email' => ->(user) { user.email },
'user.first_name' => ->(user) { user.first_name },
'user.id' => ->(user) { user.id },
'user.last_name' => ->(user) { user.last_name }
'user.phone_number' => ->(user) { user.phone_number },
'user.updated_at' => ->(user) { user.updated_at }
}
Then that matcher can be used to make your specs:
describe "Fetching the current user" do
context "with valid auth token" do
it "returns the current user" do
user = create(:user)
auth_header = { "Auth-Token" => user.auth_token }
get v1_current_user_url, {}, auth_header
expect(response_body).to be_valid_json_for_user(user)
end
end
def response_body
JSON.parse(response.body)
end
end
Installation with Rails
Add this line to your application's Gemfile:
gem 'json_rspec_match_maker', require: false
And then execute:
$ bundle
Update your rails_helper.rb
with:
# require the gem
require 'json_rspec_match_maker'
# require your custom matchers you'll be writing
Dir[Rails.root.join('spec/support/matchers/json_matchers/**/*.rb')].each do |f|
require f
end
Usage
Create a new matcher that interhits from the base class:
class AddressMatcher < JsonRspecMatchMaker::Base
end
A child class just needs to define and set the @match_definition
class AddressMatcher < JsonRspecMatchMaker::Base
def initialize(address)
@match_definition = set_match_def
super
end
end
Matchers need to be wrapped in a module we can include in our specs:
module JsonMatchers
class AddressMatcher < JsonRspecMatchMaker::Base
...
end
end
That module defines our match method:
module JsonMatchers
# class defined up here...
def be_valid_json_for_address(address)
AddressMatcher.new(address)
end
end
Which we can then use in RSpec like:
RSpec.describe 'Address serialization' do
include JsonMatchers
let(:address) { Address.new }
let(:address_json) { address.to_json }
it 'serializes the address' do
expect(address_json).to be_valid_json_for_address(address)
end
end
Here is an example of a complete matcher class:
class AddressMatcher < JsonRspecMatchMaker::Base
MATCH_DEF = {
'id' => ->(instance) { instance.id },
'description' => ->(instance) { instance.description },
'street_line_one' => ->(instance) { instance.street_line_one },
'street_line_two' => ->(instance) { instance.street_line_two },
'city' => ->(instance) { instance.city },
'state' => ->(instance) { instance.state.abbreviation },
'postal_code' => ->(instance) { instance.postal_code },
}.freeze
def initialize(address)
@match_definition = MATCH_DEF
super
end
end
In that cause, our expectations are static so we can define the match definition as a constant.
In other cases, we might want our matchers to be more dynamic so we could do something like:
class AddressMatcher < JsonRspecMatchMaker::Base
def initialize(address, state_format)
@match_definition = set_match_def(state_format)
super(address)
end
def set_match_def(state_format)
{
'state' => ->(instance) { instance.state.formatted(state_format) }
}.merge(MATCH_DEF)
end
end
Arrays are defined very similary to single objects:
{
'answers' => {
each: ->(instance) { instance.answers },
attributes: {
'id' => ->(answer) { answer.id },
'question' => ->(answer) { answer.question.text },
}
}
}
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/json_rspec_match_maker. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the JsonRspecMatchMaker project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.