JSON matchmaking for all your API testing needs.
Your API is a contract between your service and your developers. It is important for you to know exactly what your JSON API is returning to the developers in order to make sure you don't accidentally change things without updating the documentations and/or bumping the API version number. Perhaps some controller tests for your JSON endpoints would help:
# MiniTest::Unit example class UsersControllerTest < MiniTest::Unit::TestCase def test_get_a_user server_response = get '/users/chancancode.json'json = JSON.parse server_response.body assert user = json['user'] assert user_id = user['id'] assert_equal 'chancancode', user['username'] assert_equal 'Godfrey Chan', user['full_name'] assert_equal '[email protected]', user['email'] assert_equal 'Administrator', user['type'] assert_kind_of Integer, user['points'] assert_match /\Ahttps?\:\/\/.*\z/i, user['homepage'] assert posts = user['posts'] assert_kind_of Integer, posts[0]['id'] assert_equal 'Hello world!', posts[0]['subject'] assert_equal user_id, posts[0]['user_id'] assert_include posts[0]['tags'], 'announcement' assert_include posts[0]['tags'], 'welcome' assert_include posts[0]['tags'], 'introduction' assert_kind_of Integer, posts[1]['id'] assert_equal 'An awesome blog post', posts[1]['subject'] assert_equal user_id, posts[1]['user_id'] assert_include posts[0]['tags'], 'blog' assert_include posts[0]['tags'], 'life'
end end
There are many problems with this approach of JSON matching:
json_expression allows you to express the structure and content of the JSON you're expecting with very readable Ruby code while preserving the flexibility of the "manual" approach.
Add it to your Gemfile:
gem 'json_expressions'
Add this to your test/spec helper file: ```ruby
require 'json_expressions/minitest'
require 'json_expressions/rspec' ```
Which allows you to do... ```ruby
class UsersControllerTest < MiniTest::Unit::TestCase def testgetauser serverresponse = get '/users/chancancode.json'
# This is what we expect the returned JSON to look like pattern = { user: { id: :user_id, # "Capture" this value for later username: 'chancancode', # Match this exact string full_name: 'Godfrey Chan', email: '[email protected]', type: 'Administrator', points: Integer, # Any integer value homepage: /\Ahttps?\:\/\/.*\z/i, # Let's get serious created_at: wildcard_matcher, # Don't care as long as it exists updated_at: wildcard_matcher, posts: [ { id: Integer, subject: 'Hello world!', user_id: :user_id, # Match against the captured value tags: [ 'announcement', 'welcome', 'introduction' ] # Ordering of elements does not matter by default }.ignore_extra_keys!, # Skip the uninteresting stuff { id: Integer, subject: 'An awesome blog post', user_id: :user_id, tags: ['blog' , 'life'] }.ignore_extra_keys! ].ordered! # Ensure the posts are in this exact order } }matcher = assert_json_match pattern, server_response.body # Returns the Matcher object
You can use the captured values for other purposes
assert matcher.captures[:user_id] > 0
end end
describe UsersController, "#show" do it "returns a user" do pattern = # See above...
server_response = get '/users/chancancode.json'server_response.body.must_match_json_expression(pattern)
end end
describe UsersController, "#show" do it "returns a user" do pattern = # See above...
server_response = get '/users/chancancode.json'server_response.body.should match_json_expression(pattern)
end end ```
This pattern
ruby { integer: 1, float: 1.1, string: 'Hello world!', boolean: true, array: [1,2,3], object: {key1: 'value1',key2: 'value2'}, null: nil, }matches the JSON object
json { "integer": 1, "float": 1.1, "string": "Hello world!", "boolean": true, "array": [1,2,3], "object": {"key1": "value1", "key2": "value2"}, "null": null }
You can use
wildcard_matcherto ignore keys that you don't care about (other than the fact that they exist).
This pattern
ruby [ wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher ]matches the JSON array
json [ 1, 1.1, "Hello world!", true, [1,2,3], {"key1": "value1","key2": "value2"}, null]
Furthermore, because the pattern is just plain old Ruby code, you can also write:
ruby [ wildcard_matcher ] * 7
Note: Previously, the examples here uses
WILDCARD_MATCHERwhich is a constant defined on
MiniTest::Unit::TestCase. Since 0.8.0, the use of this constant is discouraged because it doesn't work for
MiniTest::Specand
RSpecdue to how Ruby scoping works for blocks. Instead,
wildcard_matcher(a method) has been added. This is now the preferred way to retrieve the wildcard matcher in order to maintain consistency among the different test frameworks.
By default, jsonexpressions uses
Object#===to match against the corresponding value in the target JSON. In most cases, this method behaves exactly the same as
Object#==. However, certain classes override this method to provide specialized behavior (notably
Regexp,
Moduleand
Range, see below). If you find this undesirable for certain classes, you can explicitly opt them out and jsonexpressions will call
Object#==instead:
# This is the default setting JsonExpressions::Matcher.skip_triple_equal_on = [ ]To add more modules/classes
JsonExpressions::Matcher.skip_triple_equal_on << MyClass
To turn this off completely
JsonExpressions::Matcher.skip_triple_equal_on = [ BasicObject ]
Since
Regexpoverrides
Object#===to mean "matches", you can use them in your patterns and json_expressions will do the right thing:
ruby { hex: /\A0x[0-9a-f]+\z/i }matches
json { "hex": "0xC0FFEE" }but not
json { "hex": "Hello world!" }
Module(and by inheritance,
Class) overrides
===to mean
instance of. You can exploit this behavior to do type matching:
ruby { integer: Integer, float: Float, string: String, boolean: Boolean, # See http://stackoverflow.com/questions/3028243/check-if-ruby-object-is-a-boolean#answer-3028378 array: Array, object: Hash, null: NilClass, }matches the JSON object
json { "integer": 1, "float": 1.1, "string": "Hello world!", "boolean": true, "array": [1,2,3], "object": {"key1": "value1", "key2": "value2"}, "null": null }
Rangeoverrides
===to mean
include?. Therefore,
ruby { day: (1..31), month: (1..12) }matches the JSON object
json { "day": 3, "month": 11 }but not
json { "day": -1, "month": 13 }
This is also helpful for comparing Floats to a certain precision.
ruby { pi: 3.141593 }won't match
json { "pi": 3.1415926536 }But this will:
ruby { pi: (3.141592..3.141593) }
Similar to how "captures" work in Regexp, you can capture the value of certain keys for later use: ```ruby matcher = JsonExpressions::Matcher.new({ key1: :key1, key2: :key2, key3: :key3 })
matcher =~ JSON.parse('{"key1":"value1", "key2":"value2", "key3":"value3"}') # => true
matcher.captures[:key1] # => "value1" matcher.captures[:key2] # => "value2" matcher.captures[:key3] # => "value3" ```
If the same symbol is used multiple times, jsonexpression will make sure they agree. This pattern ```ruby { key1: :captureme, key2: :captureme, key3: :captureme }
matchesjson { "key1": "Hello world!", "key2": "Hello world!", "key3": "Hello world!" }
but notjson { "key1": "value1", "key2": "value2", "key3": "value3" } ```
By default, all arrays and JSON objects (i.e. Ruby hashes) are assumed to be unordered. This means
ruby [ 1, 2, 3, 4, 5 ]will match
json [ 5, 3, 2, 1, 4 ]and
ruby { key1: 'value1', key2: 'value2' }will match
json { "key2": "value2", "key1": "value1" }
You can change this behavior in a case-by-case manner:
ruby { unordered_array: [1,2,3,4,5].unordered!, # calling unordered! is optional as it's the default ordered_array: [1,2,3,4,5].ordered!, unordered_hash: {a: 1, b: 2}.unordered!, ordered_hash: {a: 1, b: 2}.ordered! }
Or you can change the defaults: ```ruby
JsonExpressions::Matcher.assumeunorderedarrays = false JsonExpressions::Matcher.assumeunorderedhashes = false ```
By default, all arrays and JSON objects (i.e. Ruby hashes) are assumed to be "strict". This means any extra elements or keys in the JSON target will cause the match to fail:
ruby [ 1, 2, 3, 4, 5 ]will not match
json [ 1, 2, 3, 4, 5, 6 ]and
ruby { key1: 'value1', key2: 'value2' }will not match
json { "key1": "value1", "key2": "value2", "key3": "value3" }
You can change this behavior in a case-by-case manner:
ruby { strict_array: [1,2,3,4,5].strict!, # calling strict! is optional as it's the default forgiving_array: [1,2,3,4,5].forgiving!, strict_hash: {a: 1, b: 2}.strict!, forgiving_hash: {a: 1, b: 2}.forgiving! }
They also come with some more sensible aliases:
ruby { strict_array: [1,2,3,4,5].reject_extra_values!, forgiving_array: [1,2,3,4,5].ignore_extra_values!, strict_hash: {a: 1, b: 2}.reject_extra_keys!, forgiving_hash: {a: 1, b: 2}.ignore_extra_keys! }
Or you can change the defaults: ```ruby
JsonExpressions::Matcher.assumestrictarrays = false JsonExpressions::Matcher.assumestricthashes = false ```
The
Matcherclass itself is written in a framework-agnostic manner. This allows you to easily write custom helpers/matchers for your favorite testing framework. If you wrote an adapter for another test frameworks and you'd like to share yhat with the world, please open a Pull Request.
Please use the GitHub issue tracker for bugs and feature requests. If you could submit a pull request - that's even better!
This library is distributed under the MIT license. Please see the LICENSE file.