• Stars
    star
    416
  • Rank 104,068 (Top 3 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 12 years ago
  • Updated over 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

JSON matchmaking for all your API testing needs.

JSON Expressions

Build Status

Introduction

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:

  • It could get out of hand really quickly
  • It is not very readable
  • It flattens the structure of the JSON and it's difficult to visualize what the JSON actually looks like
  • It does not guard against extra parameters that you might have accidentally included (password hashes, credit card numbers etc)
  • Matching nested objects and arrays is tricky, especially when you don't want to enforce a particular ordering of the returned objects

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.

Dependencies

  • Ruby 1.9+

Usage

Add it to your Gemfile:

gem 'json_expressions'

Add this to your test/spec helper file:

# For MiniTest::Unit
require 'json_expressions/minitest'

# For RSpec
require 'json_expressions/rspec'

Which allows you to do...

# MiniTest::Unit example
class UsersControllerTest < MiniTest::Unit::TestCase
  def test_get_a_user
    server_response = 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

# MiniTest::Spec example
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

# RSpec example
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

Basic Matching

This pattern

{
  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

{
  "integer": 1,
  "float": 1.1,
  "string": "Hello world!",
  "boolean": true,
  "array": [1,2,3],
  "object": {"key1": "value1", "key2": "value2"},
  "null": null
}

Wildcard Matching

You can use wildcard_matcher to ignore keys that you don't care about (other than the fact that they exist).

This pattern

[ wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher ]

matches the JSON array

[ 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:

[ wildcard_matcher ] * 7

Note: Previously, the examples here uses WILDCARD_MATCHER which 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::Spec and RSpec due 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.

Object Equality

By default, json_expressions 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, Module and Range, see below). If you find this undesirable for certain classes, you can explicitly opt them out and json_expressions 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 ]

Regular Expressions

Since Regexp overrides Object#=== to mean "matches", you can use them in your patterns and json_expressions will do the right thing:

{ hex: /\A0x[0-9a-f]+\z/i }

matches

{ "hex": "0xC0FFEE" }

but not

{ "hex": "Hello world!" }

Type Matching

Module (and by inheritance, Class) overrides === to mean instance of. You can exploit this behavior to do type matching:

{
  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

{
  "integer": 1,
  "float": 1.1,
  "string": "Hello world!",
  "boolean": true,
  "array": [1,2,3],
  "object": {"key1": "value1", "key2": "value2"},
  "null": null
}

Ranges

Range overrides === to mean include?. Therefore,

{ day: (1..31), month: (1..12) }

matches the JSON object

{ "day": 3, "month": 11 }

but not

{ "day": -1, "month": 13 }

This is also helpful for comparing Floats to a certain precision.

{ pi: 3.141593 }

won't match

{ "pi": 3.1415926536 }

But this will:

{ pi: (3.141592..3.141593) }

Capturing

Similar to how "captures" work in Regexp, you can capture the value of certain keys for later use:

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, json_expression will make sure they agree. This pattern

{
  key1: :capture_me,
  key2: :capture_me,
  key3: :capture_me
}

matches

{
  "key1": "Hello world!",
  "key2": "Hello world!",
  "key3": "Hello world!"
}

but not

{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3"
}

Ordering

By default, all arrays and JSON objects (i.e. Ruby hashes) are assumed to be unordered. This means

[ 1, 2, 3, 4, 5 ]

will match

[ 5, 3, 2, 1, 4 ]

and

{ key1: 'value1', key2: 'value2' }

will match

{ "key2": "value2", "key1": "value1" }

You can change this behavior in a case-by-case manner:

{
  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:

# Default for these are true
JsonExpressions::Matcher.assume_unordered_arrays = false
JsonExpressions::Matcher.assume_unordered_hashes = false

"Strictness"

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:

[ 1, 2, 3, 4, 5 ]

will not match

[ 1, 2, 3, 4, 5, 6 ]

and

{ key1: 'value1', key2: 'value2' }

will not match

{ "key1": "value1", "key2": "value2", "key3": "value3" }

You can change this behavior in a case-by-case manner:

{
  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:

{
  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:

# Default for these are true
JsonExpressions::Matcher.assume_strict_arrays = false
JsonExpressions::Matcher.assume_strict_hashes = false

Support for other test frameworks

The Matcher class 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.

Contributing

Please use the GitHub issue tracker for bugs and feature requests. If you could submit a pull request - that's even better!

License

This library is distributed under the MIT license. Please see the LICENSE file.

More Repositories

1

hn-reader

An embitious Hacker News reader, built with Ember.js
JavaScript
207
star
2

marionette-rails

Vendors the Backbone.Marionette library for use with Rails' asset pipeline
Ruby
107
star
3

postcss-canadian-stylesheets

PostCSS plugin for writing Canadian stylesheets
JavaScript
73
star
4

branch-rename

68
star
5

javascript

Harness the raw power of your machine with JavaScript
Ruby
59
star
6

ember-concurrency-async

Async task functions for ember-concurrency
JavaScript
53
star
7

blame_parent

A chrome extension to make blaming easy on github
JavaScript
51
star
8

entypo-plus

397 carefully crafted premium pictograms by Daniel Bruce
HTML
39
star
9

ember-concurrency-ts

TypeScript utilities for ember-concurrency.
TypeScript
15
star
10

ember-bench

Ruby
10
star
11

ember-cli-canadian-stylesheets

Ember CLI Canadian Stylesheets
JavaScript
9
star
12

activesupport-json_encoder

The old JSON encoder with `encode_json` support (Removed from core in Rails 4.1)
Ruby
6
star
13

ember-github-issues

JavaScript
4
star
14

mruby-canada

Adds support for Canadian programming conventions to the mruby language
Ruby
4
star
15

rails-internals

Like chrome://net-internals, but for Rails.
Ruby
3
star
16

actionview-component

Ruby
3
star
17

reactions

Source code for https://reactions.live
JavaScript
3
star
18

stampy

TypeScript
2
star
19

hearts-judge

Judge script for our Hearts NPC Project
Ruby
2
star
20

KinectPresenter

C#
2
star
21

as_json_encoder

A JSON encoder that is tailored to the needs of Rails.
Ruby
2
star
22

mygittest

learning to use git(hub). trying stuff out.
2
star
23

angry-mum

1
star
24

activesupport-encode_json

A JSON encoder that supports Object#encode_json (removed from core in Rails 4.1)
1
star
25

Xcode-Test

testing xcode + git hub
1
star
26

freezer

Ruby
1
star
27

enjoyable

Use your gamepad or joystick like a mouse and keyboard on Mac OS X.
Objective-C
1
star
28

passive_model_serializers

An experiment to to make ActiveModel::Serializers more flexible
Ruby
1
star
29

json_caching

Ruby
1
star
30

railsbridge-stickerbot

Stickerbot!
1
star
31

yarn-bug-report

1
star
32

automatic-backport

Automatically backport commits based on commit tags or pull request labels
TypeScript
1
star
33

test-gha

1
star