This gem provides a set of matchers that make testing JSON documents (actually the hashes parsed from them) simpler and more elegant.
Oftentimes when testing your JSON API responses, you don't care about the actual values matching exactly, just that they "resemble" your expected values. This gem provides a variety of matchers to just get close:
Anything that's a Ruby Numeric
will match:
# These pass
expect(10).to resemble 42
expect(3.14).to resemble 42
expect(10).to resemble 42.4
# These fail
expect("string").to resemble 42
expect(Time.now).to resemble 42
I haven't needed it yet, but I'm open to discussing if more accurate matches would be needed. For example:
- Does 1_000_000_000 "resemble" 1?
- Does a float "resemble" an integer?
Anything that is a Ruby Date
/Time
/DateTime
, or a string that can be parsed by Time.iso8601
will match:
# These pass
expect(Time.now).to resemble "2018-01-01T00:00:00Z"
expect(Time.now.iso8601).to resemble "2018-01-01T00:00:00Z"
# These fail
expect("Some string").to resemble "2018-01-01T00:00:00Z"
Open questions:
- Does
"2018-01-01T00:00:00-0700"
"resemble""2018-01-01T00:00:00Z"
? That is, should it ensure the timezone matches? - Do non-ISO8601 datetimes "resemble" ISO8601 ones?
- Is there a permissible time range? Does the year 1600 "resemble" 2017? Does
"0000-00-00T00:00:00Z"
? DoesTime.at(0)
?
If you're using Rails (specifically ActionDispatch
), we can check that routes resemble each other:
# These pass
expect("/posts/1").to resemble posts_path(post)
expect("/posts/1000000").to resemble posts_path(post)
expect("https://example.com/posts/1").to resemble posts_path(post)
# These fail
expect("/users/1").to resemble posts_path(post)
expect("Some string").to resemble posts_path(post)
Any string that didn't match one of the other resembles matchers will match:
# These pass
expect("Some string").to resemble "some other string"
expect("").to resemble "some other string"
expect("a" * 100_000).to resemble "some other string"
# These fail
expect(42).to resemble "42"
expect(Time.now).to resemble "Time.now
Open questions:
- Should there be some heuristic to decide if a string resembles another? Does length matter?
The resembles matchers are nice on their own, but their power shines when used with the resembles_json
matcher. This allows you to write an example JSON document in your spec, and match it against the output from a request. Any values in the "expected" document that aren't already matchers will be converted to the best resembles
matcher. You can write plain json documents as the expected, or be explicit by specifying matchers.
RSpec.describe "a basic json document" do
# This would probably actually come from something like `JSON.parse(response.body)`
let(:document) do
{
"@id": "/posts/2016/test1",
"@type": "Post",
"title": "Hello World!",
"body": "lorem ipsum",
"created_at": "2016-03-01T00:03:42",
"published_at": "2016-03-10T15:35:00"
}
end
specify do
expect(document).to resemble_json(
{
"@id": post_path(post), # resembles route
"@type": eq("Post"), # using an explicit matcher to match exactly
"title": match(/^Hello/), # another explicit matcher
"body": "lorem ipsum", # resembles string
"created_at": "2016-03-01T00:03:42", # resembles time
"published_at": post.published_at # Also resembles time
}
)
end
end
It provides good descriptions if you run rspec
with --format=documentation
:
a basic json document
should resemble json
{
"@id": /posts/:year/:title,
"@type": "Post",
"title": Hello World!,
"body": lorem ipsum,
"created_at": "2016-03-01T00:03:42",
"published_at": "2016-03-10T15:35:00"
}
It also provides a failure message as a diff of the JSON object:
1) The resembles json matcher a basic json document with several attributes that failed to match should have json that looks like
{
"@id": "/posts/:year/:title",
"@type": "PostCollection",
"title": 42.0,
"body": "lorem ipsum",
"created_at": "2016-03-01T00:03:42",
"published_at": "2016-03-10T15:35:00"
}
Failure/Error:
expect(document).to resemble_json(
{
"@id": "/posts/:year/:title",
"@type": eq("PostCollection"),
"title": 42.0,
"body": "lorem ipsum",
"created_at": "2016-03-01T00:03:42",
"published_at": "2016-03-10T15:35:00"
}
)
{
"@id": "/posts/:year/:title",
- "@type": eq "PostCollection",
+ "@type": "Post",
- "title": 42.0,
+ "title": "Hello World!",
"body": "lorem ipsum",
"created_at": "2016-03-01T00:03:42",
"published_at": "2016-03-10T15:35:00"
}
# ./examples/example_spec.rb:40:in `block (4 levels) in <top (required)>'
It can also handle nested JSON documents and Arrays, showing the proper diffs. See the ./examples
directory, but it works pretty much how you'd expect.
Add this line to your application's Gemfile:
gem 'rspec-json_api_matchers'
And then execute:
$ bundle
Or install it yourself as:
$ gem install rspec-json_api_matchers
Bug reports and pull requests are welcome on GitHub at https://github.com/paul/rspec-json_api_matchers.