├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── features ├── equivalence.feature ├── files.feature ├── inclusion.feature ├── memory.feature ├── paths.feature ├── sizes.feature ├── step_definitions │ └── steps.rb ├── support │ └── env.rb └── types.feature ├── gemfiles ├── rspec2.gemfile └── rspec3.gemfile ├── json_spec.gemspec ├── lib ├── json_spec.rb └── json_spec │ ├── configuration.rb │ ├── cucumber.rb │ ├── errors.rb │ ├── exclusion.rb │ ├── helpers.rb │ ├── matchers.rb │ ├── matchers │ ├── be_json_eql.rb │ ├── have_json_path.rb │ ├── have_json_size.rb │ ├── have_json_type.rb │ └── include_json.rb │ ├── memory.rb │ └── messages.rb └── spec ├── json_spec ├── configuration_spec.rb ├── helpers_spec.rb ├── matchers │ ├── be_json_eql_spec.rb │ ├── have_json_path_spec.rb │ ├── have_json_size_spec.rb │ ├── have_json_type_spec.rb │ └── include_json_spec.rb ├── matchers_spec.rb └── memory_spec.rb ├── spec_helper.rb └── support └── files ├── one.json ├── project ├── one.json ├── two.json └── version │ ├── one.json │ └── two.json └── two.json /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | Gemfile.lock 3 | gemfiles/*.lock 4 | pkg 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | after_success: 2 | - bundle exec codeclimate-test-reporter 3 | branches: 4 | only: 5 | - master 6 | gemfile: 7 | - gemfiles/rspec2.gemfile 8 | - gemfiles/rspec3.gemfile 9 | language: ruby 10 | matrix: 11 | allow_failures: 12 | - rvm: ruby-head 13 | - rvm: "2.0" 14 | - rvm: "2.1" 15 | rvm: 16 | - "2.0" 17 | - "2.1" 18 | - "2.2.7" 19 | - "2.3.4" 20 | - "2.4.1" 21 | - ruby-head 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased (master) 2 | 3 | - Added Changelog 4 | - Fix RSpec warnings for uninitialized instance variables on matchers [#78](https://github.com/collectiveidea/json_spec/pull/78) 5 | 6 | ## Version 1.1.4 7 | 8 | - Raise error when checking a size of a json ruby value of nil [#82](https://github.com/collectiveidea/json_spec/pull/82) 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem "codeclimate-test-reporter", "~> 1.0.0" 7 | gem "cucumber", "~> 1.3" 8 | end 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2011 Steve Richert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json_spec 2 | 3 | Easily handle JSON in RSpec and Cucumber 4 | 5 | [![Gem Version](https://img.shields.io/gem/v/json_spec.svg?style=flat)](http://rubygems.org/gems/json_spec) 6 | [![Build Status](https://img.shields.io/travis/collectiveidea/json_spec/master.svg?style=flat)](https://travis-ci.org/collectiveidea/json_spec) 7 | [![Code Climate](https://img.shields.io/codeclimate/github/collectiveidea/json_spec.svg?style=flat)](https://codeclimate.com/github/collectiveidea/json_spec) 8 | [![Dependency Status](https://img.shields.io/gemnasium/collectiveidea/json_spec.svg?style=flat)](https://gemnasium.com/collectiveidea/json_spec) 9 | 10 | ## RSpec 11 | 12 | json_spec defines five new RSpec matchers: 13 | 14 | * `be_json_eql` 15 | * `include_json` 16 | * `have_json_path` 17 | * `have_json_type` 18 | * `have_json_size` 19 | 20 | The new matchers could be used in RSpec as follows: 21 | 22 | ```ruby 23 | describe User do 24 | let(:user){ User.create!(first_name: "Steve", last_name: "Richert") } 25 | 26 | context "#to_json" do 27 | it "includes names" do 28 | names = %({"first_name":"Steve","last_name":"Richert"}) 29 | user.to_json.should be_json_eql(names).excluding("friends") 30 | end 31 | 32 | it "includes the ID" do 33 | user.to_json.should have_json_path("id") 34 | user.to_json.should have_json_type(Integer).at_path("id") 35 | end 36 | 37 | it "includes friends" do 38 | user.to_json.should have_json_size(0).at_path("friends") 39 | 40 | friend = User.create!(first_name: "Catie", last_name: "Richert") 41 | user.friends << friend 42 | 43 | user.to_json.should have_json_size(1).at_path("friends") 44 | user.to_json.should include_json(friend.to_json) 45 | end 46 | end 47 | end 48 | ``` 49 | 50 | json_spec also provides some useful helpers for RSpec tests: 51 | 52 | * `parse_json` 53 | * `normalize_json` 54 | * `generate_normalized_json` 55 | * `load_json` 56 | 57 | To start using them add an include them in your RSpec configuration: 58 | 59 | ```ruby 60 | RSpec.configure do |config| 61 | config.include JsonSpec::Helpers 62 | end 63 | ``` 64 | 65 | You can find usage examples for the helpers in [`spec/json_spec/helpers_spec.rb`](https://github.com/collectiveidea/json_spec/blob/master/spec/json_spec/helpers_spec.rb) 66 | 67 | ### Exclusions 68 | 69 | json_spec ignores certain hash keys by default when comparing JSON: 70 | 71 | * `id` 72 | * `created_at` 73 | * `updated_at` 74 | 75 | It's oftentimes helpful when evaluating JSON representations of newly-created ActiveRecord records 76 | so that the new ID and timestamps don't have to be known. These exclusions are globally 77 | customizeable: 78 | 79 | ```ruby 80 | JsonSpec.configure do 81 | exclude_keys "created_at", "updated_at" 82 | end 83 | ``` 84 | 85 | Now, the `id` key will be included in json_spec's comparisons. Keys can also be excluded/included 86 | per matcher by chaining the `excluding` or `including` methods (as shown above) which will add or 87 | subtract from the globally excluded keys, respectively. 88 | 89 | ### Paths 90 | 91 | Each of json_spec's matchers deal with JSON "paths." These are simple strings of "/" separated 92 | hash keys and array indexes. For instance, with the following JSON: 93 | 94 | { 95 | "first_name": "Steve", 96 | "last_name": "Richert", 97 | "friends": [ 98 | { 99 | "first_name": "Catie", 100 | "last_name": "Richert" 101 | } 102 | ] 103 | } 104 | 105 | We could access the first friend's first name with the path `"friends/0/first_name"`. 106 | 107 | ## Cucumber 108 | 109 | json_spec provides Cucumber steps that utilize its RSpec matchers and that's where json_spec really 110 | shines. This is perfect for testing your app's JSON API. 111 | 112 | In order to use the Cucumber steps, in your `env.rb` you must: 113 | 114 | ```ruby 115 | require "json_spec/cucumber" 116 | ``` 117 | 118 | You also need to define a `last_json` method. If you're using Capybara, it could be as simple as: 119 | 120 | ```ruby 121 | def last_json 122 | page.source 123 | end 124 | ``` 125 | 126 | Now, you can use the json_spec steps in your features: 127 | 128 | ```cucumber 129 | Feature: User API 130 | Background: 131 | Given the following users exist: 132 | | id | first_name | last_name | 133 | | 1 | Steve | Richert | 134 | | 2 | Catie | Richert | 135 | And "Steve Richert" is friends with "Catie Richert" 136 | 137 | Scenario: Index action 138 | When I visit "/users.json" 139 | Then the JSON response should have 2 users 140 | And the JSON response at "0/id" should be 1 141 | And the JSON response at "1/id" should be 2 142 | 143 | Scenario: Show action 144 | When I visit "/users/1.json" 145 | Then the JSON response at "first_name" should be "Steve" 146 | And the JSON response at "last_name" should be "Richert" 147 | And the JSON response should have "created_at" 148 | And the JSON response at "created_at" should be a string 149 | And the JSON response at "friends" should be: 150 | """ 151 | [ 152 | { 153 | "id": 2, 154 | "first_name": "Catie", 155 | "last_name": "Richert" 156 | } 157 | ] 158 | """ 159 | ``` 160 | 161 | The background steps above aren't provided by json_spec and the "visit" steps are provided by 162 | Capybara. The remaining steps, json_spec provides. They're versatile and can be used in plenty of 163 | different formats: 164 | 165 | ```cucumber 166 | Then the JSON should be: 167 | """ 168 | { 169 | "key": "value" 170 | } 171 | """ 172 | Then the JSON at "path" should be: 173 | """ 174 | [ 175 | "entry", 176 | "entry" 177 | ] 178 | """ 179 | 180 | Then the JSON should be {"key":"value"} 181 | Then the JSON at "path" should be {"key":"value"} 182 | Then the JSON should be ["entry","entry"] 183 | Then the JSON at "path" should be ["entry","entry"] 184 | Then the JSON at "path" should be "string" 185 | Then the JSON at "path" should be 10 186 | Then the JSON at "path" should be 10.0 187 | Then the JSON at "path" should be 1e+1 188 | Then the JSON at "path" should be true 189 | Then the JSON at "path" should be false 190 | Then the JSON at "path" should be null 191 | 192 | Then the JSON should include: 193 | """ 194 | { 195 | "key": "value" 196 | } 197 | """ 198 | Then the JSON at "path" should include: 199 | """ 200 | [ 201 | "entry", 202 | "entry" 203 | ] 204 | """ 205 | 206 | Then the JSON should include {"key":"value"} 207 | Then the JSON at "path" should include {"key":"value"} 208 | Then the JSON should include ["entry","entry"] 209 | Then the JSON at "path" should include ["entry","entry"] 210 | Then the JSON should include "string" 211 | Then the JSON at "path" should include "string" 212 | Then the JSON should include 10 213 | Then the JSON at "path" should include 10 214 | Then the JSON should include 10.0 215 | Then the JSON at "path" should include 10.0 216 | Then the JSON should include 1e+1 217 | Then the JSON at "path" should include 1e+1 218 | Then the JSON should include true 219 | Then the JSON at "path" should include true 220 | Then the JSON should include false 221 | Then the JSON at "path" should include false 222 | Then the JSON should include null 223 | Then the JSON at "path" should include null 224 | 225 | Then the JSON should have "path" 226 | 227 | Then the JSON should be a hash 228 | Then the JSON at "path" should be an array 229 | Then the JSON at "path" should be a float 230 | 231 | Then the JSON should have 1 entry 232 | Then the JSON at "path" should have 2 entries 233 | Then the JSON should have 3 keys 234 | Then the JSON should have 4 whatevers 235 | ``` 236 | 237 | _All instances of "should" above could be followed by "not" and all instances of "JSON" could be downcased and/or followed by "response."_ 238 | 239 | ### Table Format 240 | 241 | Another step exists that uses Cucumber's table formatting and wraps two of the above steps: 242 | 243 | ```cucumber 244 | Then the JSON should have the following: 245 | | path/0 | {"key":"value"} | 246 | | path/1 | ["entry","entry"] | 247 | ``` 248 | 249 | Any number of rows can be given. The step above is equivalent to: 250 | 251 | ```cucumber 252 | Then the JSON at "path/0" should be {"key":"value"} 253 | And the JSON at "path/1" should be ["entry","entry"] 254 | ``` 255 | 256 | If only one column is given: 257 | 258 | ```cucumber 259 | Then the JSON should have the following: 260 | | path/0 | 261 | | path/1 | 262 | ``` 263 | 264 | This is equivalent to: 265 | 266 | ```cucumber 267 | Then the JSON should have "path/0" 268 | And the JSON should have "path/1" 269 | ``` 270 | 271 | ### JSON Memory 272 | 273 | There's one more Cucumber step that json_spec provides which hasn't been used above. It's used to 274 | memorize JSON for reuse in later steps. You can "keep" all or a portion of the JSON by giving a 275 | name by which to remember it. 276 | 277 | ```cucumber 278 | Feature: User API 279 | Scenario: Index action includes full user JSON 280 | Given the following user exists: 281 | | id | first_name | last_name | 282 | | 1 | Steve | Richert | 283 | And I visit "/users/1.json" 284 | And I keep the JSON response as "USER_1" 285 | When I visit "/users.json" 286 | Then the JSON response should be: 287 | """ 288 | [ 289 | %{USER_1} 290 | ] 291 | """ 292 | ``` 293 | 294 | You can memorize JSON at a path: 295 | 296 | ```cucumber 297 | Given I keep the JSON response at "first_name" as "FIRST_NAME" 298 | ``` 299 | 300 | You can remember JSON at a path: 301 | 302 | ```cucumber 303 | Then the JSON response at "0/first_name" should be: 304 | """ 305 | %{FIRST_NAME} 306 | """ 307 | ``` 308 | 309 | You can also remember JSON inline: 310 | 311 | ```cucumber 312 | Then the JSON response at "0/first_name" should be %{FIRST_NAME} 313 | ``` 314 | 315 | ### More 316 | 317 | Check out the [specs](https://github.com/collectiveidea/json_spec/blob/master/spec) 318 | and [features](https://github.com/collectiveidea/json_spec/blob/master/features) to see all the 319 | various ways you can use json_spec. 320 | 321 | ## Contributing 322 | 323 | If you come across any issues, please [tell us](https://github.com/collectiveidea/json_spec/issues). 324 | Pull requests (with tests) are appreciated. No pull request is too small. Please help with: 325 | 326 | * Reporting bugs 327 | * Suggesting features 328 | * Writing or improving documentation 329 | * Fixing typos 330 | * Cleaning whitespace 331 | * Refactoring code 332 | * Adding tests 333 | * Closing [issues](https://github.com/collectiveidea/json_spec/issues) 334 | 335 | If you report a bug and don't include a fix, please include a failing test. 336 | 337 | ## Copyright 338 | 339 | Copyright © 2011 Steve Richert 340 | 341 | See [LICENSE](https://github.com/collectiveidea/json_spec/blob/master/LICENSE) for details. 342 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | require "cucumber/rake/task" 4 | 5 | RSpec::Core::RakeTask.new(:spec) do |task| 6 | task.rspec_opts = "--warnings" 7 | end 8 | 9 | Cucumber::Rake::Task.new(:cucumber) do |task| 10 | task.cucumber_opts = "--tags ~@fail" 11 | end 12 | 13 | Cucumber::Rake::Task.new(:negative_cucumber) do |task| 14 | task.cucumber_opts = "--tags @fail --wip" 15 | end 16 | 17 | task test: [:spec, :cucumber, :negative_cucumber] 18 | task default: :test 19 | -------------------------------------------------------------------------------- /features/equivalence.feature: -------------------------------------------------------------------------------- 1 | Feature: Equivalence 2 | Background: 3 | Given the JSON is: 4 | """ 5 | { 6 | "array": [ 7 | "json", 8 | "spec" 9 | ], 10 | "created_at": "2011-07-08 02:27:34", 11 | "empty_array": [ 12 | 13 | ], 14 | "empty_hash": { 15 | }, 16 | "false": false, 17 | "float": 10.0, 18 | "hash": { 19 | "json": "spec" 20 | }, 21 | "id": 1, 22 | "integer": 10, 23 | "negative": -10, 24 | "null": null, 25 | "string": "json_spec", 26 | "true": true, 27 | "updated_at": "2011-07-08 02:28:50" 28 | } 29 | """ 30 | 31 | Scenario: Identical JSON 32 | When I get the JSON 33 | Then the JSON should be: 34 | """ 35 | { 36 | "array": [ 37 | "json", 38 | "spec" 39 | ], 40 | "created_at": "2011-07-08 02:27:34", 41 | "empty_array": [ 42 | 43 | ], 44 | "empty_hash": { 45 | }, 46 | "false": false, 47 | "float": 10.0, 48 | "hash": { 49 | "json": "spec" 50 | }, 51 | "id": 1, 52 | "integer": 10, 53 | "negative": -10, 54 | "null": null, 55 | "string": "json_spec", 56 | "true": true, 57 | "updated_at": "2011-07-08 02:28:50" 58 | } 59 | """ 60 | 61 | Scenario: Reverse order 62 | When I get the JSON 63 | Then the JSON should be: 64 | """ 65 | { 66 | "updated_at": "2011-07-08 02:28:50", 67 | "true": true, 68 | "string": "json_spec", 69 | "null": null, 70 | "negative": -10, 71 | "integer": 10, 72 | "id": 1, 73 | "hash": { 74 | "json": "spec" 75 | }, 76 | "float": 10.0, 77 | "false": false, 78 | "empty_hash": { 79 | }, 80 | "empty_array": [ 81 | 82 | ], 83 | "created_at": "2011-07-08 02:27:34", 84 | "array": [ 85 | "json", 86 | "spec" 87 | ] 88 | } 89 | """ 90 | 91 | Scenario: Excluding keys 92 | When I get the JSON 93 | Then the JSON should be: 94 | """ 95 | { 96 | "array": [ 97 | "json", 98 | "spec" 99 | ], 100 | "empty_array": [ 101 | 102 | ], 103 | "empty_hash": { 104 | }, 105 | "false": false, 106 | "float": 10.0, 107 | "hash": { 108 | "json": "spec" 109 | }, 110 | "integer": 10, 111 | "negative": -10, 112 | "null": null, 113 | "string": "json_spec", 114 | "true": true 115 | } 116 | """ 117 | 118 | Scenario: String 119 | When I get the JSON 120 | Then the JSON at "string" should be "json_spec" 121 | And the JSON at "string" should be: 122 | """ 123 | "json_spec" 124 | """ 125 | 126 | Scenario: Integer 127 | When I get the JSON 128 | Then the JSON at "integer" should be 10 129 | And the JSON at "integer" should be: 130 | """ 131 | 10 132 | """ 133 | 134 | Scenario: Negative integer 135 | When I get the JSON 136 | Then the JSON at "negative" should be -10 137 | And the JSON at "negative" should be: 138 | """ 139 | -10 140 | """ 141 | 142 | Scenario: Float 143 | When I get the JSON 144 | Then the JSON at "float" should be 10.0 145 | And the JSON at "float" should be 10.0e0 146 | And the JSON at "float" should be 10.0e+0 147 | And the JSON at "float" should be 10.0e-0 148 | And the JSON at "float" should be 10e0 149 | And the JSON at "float" should be 10e+0 150 | And the JSON at "float" should be 10e-0 151 | And the JSON at "float" should be 1.0e1 152 | And the JSON at "float" should be 1.0e+1 153 | And the JSON at "float" should be 1e1 154 | And the JSON at "float" should be 1e+1 155 | And the JSON at "float" should be 100.0e-1 156 | And the JSON at "float" should be 100e-1 157 | And the JSON at "float" should be: 158 | """ 159 | 10.0 160 | """ 161 | 162 | Scenario: Array 163 | When I get the JSON 164 | Then the JSON at "array" should be ["json","spec"] 165 | And the JSON at "array/0" should be "json" 166 | And the JSON at "array/1" should be "spec" 167 | And the JSON at "array" should be: 168 | """ 169 | [ 170 | "json", 171 | "spec" 172 | ] 173 | """ 174 | 175 | Scenario: Empty array 176 | When I get the JSON 177 | Then the JSON at "empty_array" should be [] 178 | And the JSON at "empty_array" should be: 179 | """ 180 | [ 181 | 182 | ] 183 | """ 184 | 185 | Scenario: Hash 186 | When I get the JSON 187 | Then the JSON at "hash" should be {"json":"spec"} 188 | And the JSON at "hash/json" should be "spec" 189 | And the JSON at "hash" should be: 190 | """ 191 | { 192 | "json": "spec" 193 | } 194 | """ 195 | 196 | Scenario: Empty hash 197 | When I get the JSON 198 | Then the JSON at "empty_hash" should be {} 199 | And the JSON at "empty_hash" should be: 200 | """ 201 | { 202 | } 203 | """ 204 | 205 | Scenario: True 206 | When I get the JSON 207 | Then the JSON at "true" should be true 208 | And the JSON at "true" should be: 209 | """ 210 | true 211 | """ 212 | 213 | Scenario: False 214 | When I get the JSON 215 | Then the JSON at "false" should be false 216 | And the JSON at "false" should be: 217 | """ 218 | false 219 | """ 220 | 221 | Scenario: Null 222 | When I get the JSON 223 | Then the JSON at "null" should be null 224 | And the JSON at "null" should be: 225 | """ 226 | null 227 | """ 228 | 229 | Scenario: Excluded value 230 | When I get the JSON 231 | Then the JSON at "created_at" should be "2011-07-08 02:27:34" 232 | And the JSON at "id" should be 1 233 | And the JSON at "updated_at" should be "2011-07-08 02:28:50" 234 | 235 | Scenario: Table format 236 | When I get the JSON 237 | Then the JSON should have the following: 238 | | array | ["json","spec"] | 239 | | array/0 | "json" | 240 | | array/1 | "spec" | 241 | | created_at | "2011-07-08 02:27:34" | 242 | | empty_array | [] | 243 | | empty_hash | {} | 244 | | false | false | 245 | | float | 10.0 | 246 | | hash | {"json":"spec"} | 247 | | hash/json | "spec" | 248 | | id | 1 | 249 | | integer | 10 | 250 | | negative | -10 | 251 | | null | null | 252 | | string | "json_spec" | 253 | | true | true | 254 | | updated_at | "2011-07-08 02:28:50" | 255 | And the JSON at "array" should have the following: 256 | | 0 | "json" | 257 | | 1 | "spec" | 258 | And the JSON at "hash" should have the following: 259 | | json | "spec" | 260 | 261 | @fail 262 | Scenario: Table format can fail equivalence 263 | When I get the JSON 264 | Then the JSON should have the following: 265 | | array | ["bad","garbage"] | 266 | | array/0 | "json" | 267 | | array/1 | "spec" | 268 | | created_at | "2011-07-08 02:27:34" | 269 | | empty_array | [] | 270 | | empty_hash | {} | 271 | | false | false | 272 | | float | 10.0 | 273 | | hash | {"json":"spec"} | 274 | | hash/json | "spec" | 275 | | id | 1 | 276 | | integer | 10 | 277 | | negative | -10 | 278 | | null | null | 279 | | string | "json_spec" | 280 | | true | true | 281 | | updated_at | "2011-07-08 02:28:50" | 282 | And the JSON at "array" should have the following: 283 | | should | "fail" | 284 | | 1 | "spec" | 285 | And the JSON at "hash" should have the following: 286 | | random | "junk" | 287 | -------------------------------------------------------------------------------- /features/files.feature: -------------------------------------------------------------------------------- 1 | Feature: Files 2 | Scenario: Equivalence from a file 3 | Given the JSON is: 4 | """ 5 | { 6 | "array": [ 7 | "json", 8 | "spec" 9 | ], 10 | "false": false, 11 | "float": 10.0, 12 | "hash": { 13 | "json": "spec" 14 | }, 15 | "created_at": "2011-07-08 02:27:34", 16 | "empty_array": [ 17 | 18 | ], 19 | "empty_hash": { 20 | }, 21 | "id": 1, 22 | "integer": 10, 23 | "negative": -10, 24 | "null": null, 25 | "string": "json_spec", 26 | "true": true, 27 | "updated_at": "2011-07-08 02:28:50" 28 | } 29 | """ 30 | When I get the JSON 31 | Then the JSON should be file "two.json" 32 | 33 | Scenario: Inequivalence from a file 34 | Given the JSON is: 35 | """ 36 | { 37 | "string": "json_spec", 38 | "true": true, 39 | "updated_at": "2011-07-08 02:28:50" 40 | } 41 | """ 42 | When I get the JSON 43 | Then the JSON should not be file "two.json" 44 | 45 | 46 | Scenario: Inclusion from a file 47 | Given the JSON is: 48 | """ 49 | { 50 | "array": [ 51 | "json", 52 | "spec" 53 | ], 54 | "created_at": "2011-07-08 02:27:34", 55 | "empty_array": [ 56 | 57 | ], 58 | "empty_hash": { 59 | }, 60 | "false": false, 61 | "float": 10.0, 62 | "hash": { 63 | "json": "spec" 64 | } 65 | } 66 | """ 67 | When I get the JSON 68 | Then the JSON should include file "project/version/two.json" 69 | 70 | Scenario: Exclusion from a file 71 | Given the JSON is: 72 | """ 73 | { 74 | "array": [ 75 | "json", 76 | "spec" 77 | ], 78 | "created_at": "2011-07-08 02:27:34", 79 | "empty_array": [ 80 | 81 | ], 82 | "empty_hash": { 83 | }, 84 | "false": false, 85 | "float": 10.0 86 | } 87 | """ 88 | When I get the JSON 89 | Then the JSON should not include file "project/version/two.json" 90 | -------------------------------------------------------------------------------- /features/inclusion.feature: -------------------------------------------------------------------------------- 1 | Feature: Inclusion 2 | Background: 3 | Given the JSON is: 4 | """ 5 | { 6 | "array": [ 7 | "json", 8 | "spec" 9 | ], 10 | "created_at": "2011-07-08 02:27:34", 11 | "empty_array": [ 12 | 13 | ], 14 | "empty_hash": { 15 | }, 16 | "false": false, 17 | "float": 10.0, 18 | "hash": { 19 | "json": "spec" 20 | }, 21 | "id": 1, 22 | "integer": 10, 23 | "negative": -10, 24 | "null": null, 25 | "string": "json_spec", 26 | "true": true, 27 | "updated_at": "2011-07-08 02:28:50", 28 | "nested": { 29 | "id": 2, 30 | "key": "value" 31 | } 32 | } 33 | """ 34 | 35 | Scenario: String 36 | When I get the JSON 37 | Then the JSON should include "json_spec" 38 | And the JSON should include: 39 | """ 40 | "json_spec" 41 | """ 42 | 43 | Scenario: Integer 44 | When I get the JSON 45 | Then the JSON should include 10 46 | And the JSON should include: 47 | """ 48 | 10 49 | """ 50 | 51 | Scenario: Negative integer 52 | When I get the JSON 53 | Then the JSON should include -10 54 | And the JSON should include: 55 | """ 56 | -10 57 | """ 58 | 59 | Scenario: Float 60 | When I get the JSON 61 | Then the JSON should include 10.0 62 | And the JSON should include 10.0e0 63 | And the JSON should include 10.0e+0 64 | And the JSON should include 10.0e-0 65 | And the JSON should include 10e0 66 | And the JSON should include 10e+0 67 | And the JSON should include 10e-0 68 | And the JSON should include 1.0e1 69 | And the JSON should include 1.0e+1 70 | And the JSON should include 1e1 71 | And the JSON should include 1e+1 72 | And the JSON should include 100.0e-1 73 | And the JSON should include 100e-1 74 | And the JSON should include: 75 | """ 76 | 10.0 77 | """ 78 | 79 | Scenario: Array 80 | When I get the JSON 81 | Then the JSON should include ["json","spec"] 82 | And the JSON at "array" should include "json" 83 | And the JSON at "array" should include "spec" 84 | And the JSON should include: 85 | """ 86 | [ 87 | "json", 88 | "spec" 89 | ] 90 | """ 91 | 92 | Scenario: Empty array 93 | When I get the JSON 94 | Then the JSON should include [] 95 | And the JSON should include: 96 | """ 97 | [ 98 | 99 | ] 100 | """ 101 | 102 | Scenario: Hash 103 | When I get the JSON 104 | Then the JSON should include {"json":"spec"} 105 | And the JSON at "hash" should include "spec" 106 | And the JSON should include: 107 | """ 108 | { 109 | "json": "spec" 110 | } 111 | """ 112 | 113 | Scenario: Empty hash 114 | When I get the JSON 115 | Then the JSON should include {} 116 | And the JSON should include: 117 | """ 118 | { 119 | } 120 | """ 121 | 122 | Scenario: True 123 | When I get the JSON 124 | Then the JSON should include true 125 | And the JSON should include: 126 | """ 127 | true 128 | """ 129 | 130 | Scenario: False 131 | When I get the JSON 132 | Then the JSON should include false 133 | And the JSON should include: 134 | """ 135 | false 136 | """ 137 | 138 | Scenario: Null 139 | When I get the JSON 140 | Then the JSON should include null 141 | And the JSON should include: 142 | """ 143 | null 144 | """ 145 | 146 | Scenario: Excluded value 147 | When I get the JSON 148 | Then the JSON should include "2011-07-08 02:27:34" 149 | And the JSON should include 1 150 | And the JSON should include "2011-07-08 02:28:50" 151 | 152 | Scenario: Nested exclusions 153 | When I get the JSON 154 | Then the JSON should include {"key":"value"} 155 | -------------------------------------------------------------------------------- /features/memory.feature: -------------------------------------------------------------------------------- 1 | Feature: Memory 2 | Background: 3 | Given the JSON is: 4 | """ 5 | { 6 | "array": [ 7 | 8 | ], 9 | "false": false, 10 | "float": 10.0, 11 | "hash": { 12 | }, 13 | "integer": 10, 14 | "null": null, 15 | "string": "json_spec", 16 | "true": true 17 | } 18 | """ 19 | And I get the JSON 20 | 21 | Scenario: Entire JSON 22 | When I keep the JSON as "JSON" 23 | Then the JSON should be %{JSON} 24 | And the JSON should be: 25 | """ 26 | %{JSON} 27 | """ 28 | 29 | Scenario: String 30 | When I keep the JSON at "string" as "STRING" 31 | Then the JSON at "string" should be %{STRING} 32 | And the JSON should be: 33 | """ 34 | { 35 | "array": [ 36 | 37 | ], 38 | "false": false, 39 | "float": 10.0, 40 | "hash": { 41 | }, 42 | "integer": 10, 43 | "null": null, 44 | "string": %{STRING}, 45 | "true": true 46 | } 47 | """ 48 | 49 | Scenario: Integer 50 | When I keep the JSON at "integer" as "INTEGER" 51 | Then the JSON at "integer" should be %{INTEGER} 52 | And the JSON should be: 53 | """ 54 | { 55 | "array": [ 56 | 57 | ], 58 | "false": false, 59 | "float": 10.0, 60 | "hash": { 61 | }, 62 | "integer": %{INTEGER}, 63 | "null": null, 64 | "string": "json_spec", 65 | "true": true 66 | } 67 | """ 68 | 69 | Scenario: Float 70 | When I keep the JSON at "float" as "FLOAT" 71 | Then the JSON at "float" should be %{FLOAT} 72 | And the JSON should be: 73 | """ 74 | { 75 | "array": [ 76 | 77 | ], 78 | "false": false, 79 | "float": %{FLOAT}, 80 | "hash": { 81 | }, 82 | "integer": 10, 83 | "null": null, 84 | "string": "json_spec", 85 | "true": true 86 | } 87 | """ 88 | 89 | Scenario: Array 90 | When I keep the JSON at "array" as "ARRAY" 91 | Then the JSON at "array" should be %{ARRAY} 92 | And the JSON should be: 93 | """ 94 | { 95 | "array": %{ARRAY}, 96 | "false": false, 97 | "float": 10.0, 98 | "hash": { 99 | }, 100 | "integer": 10, 101 | "null": null, 102 | "string": "json_spec", 103 | "true": true 104 | } 105 | """ 106 | 107 | Scenario: Hash 108 | When I keep the JSON at "hash" as "HASH" 109 | Then the JSON at "hash" should be %{HASH} 110 | And the JSON should be: 111 | """ 112 | { 113 | "array": [ 114 | 115 | ], 116 | "false": false, 117 | "float": 10.0, 118 | "hash": %{HASH}, 119 | "integer": 10, 120 | "null": null, 121 | "string": "json_spec", 122 | "true": true 123 | } 124 | """ 125 | 126 | Scenario: True 127 | When I keep the JSON at "true" as "TRUE" 128 | Then the JSON at "true" should be %{TRUE} 129 | And the JSON should be: 130 | """ 131 | { 132 | "array": [ 133 | 134 | ], 135 | "false": false, 136 | "float": 10.0, 137 | "hash": { 138 | }, 139 | "integer": 10, 140 | "null": null, 141 | "string": "json_spec", 142 | "true": %{TRUE} 143 | } 144 | """ 145 | 146 | Scenario: False 147 | When I keep the JSON at "false" as "FALSE" 148 | Then the JSON at "false" should be %{FALSE} 149 | And the JSON should be: 150 | """ 151 | { 152 | "array": [ 153 | 154 | ], 155 | "false": %{FALSE}, 156 | "float": 10.0, 157 | "hash": { 158 | }, 159 | "integer": 10, 160 | "null": null, 161 | "string": "json_spec", 162 | "true": true 163 | } 164 | """ 165 | 166 | Scenario: Null 167 | When I keep the JSON at "null" as "NULL" 168 | Then the JSON at "null" should be %{NULL} 169 | And the JSON should be: 170 | """ 171 | { 172 | "array": [ 173 | 174 | ], 175 | "false": false, 176 | "float": 10.0, 177 | "hash": { 178 | }, 179 | "integer": 10, 180 | "null": %{NULL}, 181 | "string": "json_spec", 182 | "true": true 183 | } 184 | """ 185 | 186 | Scenario: Table format 187 | When I keep the JSON at "string" as "STRING" 188 | And I keep the JSON at "integer" as "INTEGER" 189 | And I keep the JSON at "float" as "FLOAT" 190 | And I keep the JSON at "array" as "ARRAY" 191 | And I keep the JSON at "hash" as "HASH" 192 | And I keep the JSON at "true" as "TRUE" 193 | And I keep the JSON at "false" as "FALSE" 194 | And I keep the JSON at "null" as "NULL" 195 | Then the JSON should have the following: 196 | | string | %{STRING} | 197 | | integer | %{INTEGER} | 198 | | float | %{FLOAT} | 199 | | array | %{ARRAY} | 200 | | hash | %{HASH} | 201 | | true | %{TRUE} | 202 | | false | %{FALSE} | 203 | | null | %{NULL} | 204 | 205 | Scenario: Inclusion 206 | When I keep the JSON at "string" as "STRING" 207 | And I keep the JSON at "integer" as "INTEGER" 208 | And I keep the JSON at "float" as "FLOAT" 209 | And I keep the JSON at "array" as "ARRAY" 210 | And I keep the JSON at "hash" as "HASH" 211 | And I keep the JSON at "true" as "TRUE" 212 | And I keep the JSON at "false" as "FALSE" 213 | And I keep the JSON at "null" as "NULL" 214 | Then the JSON should include %{STRING} 215 | And the JSON should include %{INTEGER} 216 | And the JSON should include %{FLOAT} 217 | And the JSON should include %{ARRAY} 218 | And the JSON should include %{HASH} 219 | And the JSON should include %{TRUE} 220 | And the JSON should include %{FALSE} 221 | And the JSON should include %{NULL} 222 | -------------------------------------------------------------------------------- /features/paths.feature: -------------------------------------------------------------------------------- 1 | Feature: Paths 2 | Background: 3 | Given the JSON is: 4 | """ 5 | { 6 | "array": [ 7 | { 8 | "one": 1, 9 | "two": 2 10 | }, 11 | { 12 | "four": 4, 13 | "three": 3 14 | } 15 | ], 16 | "hash": { 17 | "even": [ 18 | 6, 19 | 8 20 | ], 21 | "odd": [ 22 | 5, 23 | 7 24 | ] 25 | }, 26 | "id": null 27 | } 28 | """ 29 | 30 | Scenario: Base paths 31 | When I get the JSON 32 | Then the JSON should have "array" 33 | And the JSON should have "hash" 34 | And the JSON should have "id" 35 | 36 | Scenario: Nested paths 37 | When I get the JSON 38 | Then the JSON should have "array/0" 39 | And the JSON should have "array/1" 40 | And the JSON should have "hash/even" 41 | And the JSON should have "hash/odd" 42 | 43 | Scenario: Deeply nested paths 44 | When I get the JSON 45 | Then the JSON should have "array/0/one" 46 | And the JSON should have "array/0/two" 47 | And the JSON should have "array/1/four" 48 | And the JSON should have "array/1/three" 49 | And the JSON should have "hash/even/0" 50 | And the JSON should have "hash/even/1" 51 | And the JSON should have "hash/odd/0" 52 | And the JSON should have "hash/odd/1" 53 | 54 | Scenario: Ignored paths 55 | When I get the JSON 56 | Then the JSON should have "id" 57 | 58 | Scenario: Table format 59 | When I get the JSON 60 | Then the JSON should have the following: 61 | | array | 62 | | hash | 63 | | array/0 | 64 | | array/1 | 65 | | hash/even | 66 | | hash/odd | 67 | | array/0/one | 68 | | array/0/two | 69 | | array/1/four | 70 | | array/1/three | 71 | | hash/even/0 | 72 | | hash/even/1 | 73 | | hash/odd/0 | 74 | | hash/odd/1 | 75 | -------------------------------------------------------------------------------- /features/sizes.feature: -------------------------------------------------------------------------------- 1 | Feature: Sizes 2 | Background: 3 | Given the JSON is: 4 | """ 5 | { 6 | "id": null, 7 | "one": [ 8 | 1 9 | ], 10 | "three": [ 11 | 1, 12 | 2, 13 | 3 14 | ], 15 | "two": [ 16 | 1, 17 | 2 18 | ], 19 | "zero": [ 20 | 21 | ] 22 | } 23 | """ 24 | 25 | Scenario: Hash 26 | When I get the JSON 27 | Then the JSON should have 5 keys 28 | And the JSON should have 5 values 29 | 30 | Scenario: Empty array 31 | When I get the JSON 32 | Then the JSON at "zero" should have 0 entries 33 | 34 | Scenario: Array 35 | When I get the JSON 36 | Then the JSON at "one" should have 1 entry 37 | And the JSON at "two" should have 2 values 38 | And the JSON at "three" should have 3 numbers 39 | -------------------------------------------------------------------------------- /features/step_definitions/steps.rb: -------------------------------------------------------------------------------- 1 | Given "the JSON is:" do |json| 2 | @json = json 3 | end 4 | 5 | When "I get the JSON" do 6 | @last_json = @json 7 | end 8 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path("../../../lib", __FILE__) 2 | 3 | require "json_spec/cucumber" 4 | 5 | JsonSpec.directory = File.expand_path("../../../spec/support/files", __FILE__) 6 | 7 | def last_json 8 | @last_json.to_s 9 | end 10 | -------------------------------------------------------------------------------- /features/types.feature: -------------------------------------------------------------------------------- 1 | Feature: Types 2 | Scenario: All types 3 | Given the JSON is: 4 | """ 5 | { 6 | "array": [], 7 | "false": true, 8 | "float": 10.0, 9 | "hash": {}, 10 | "integer": 10, 11 | "string": "json_spec", 12 | "true": true 13 | } 14 | """ 15 | When I get the JSON 16 | Then the JSON should be a hash 17 | And the JSON at "array" should be an array 18 | And the JSON at "false" should be a boolean 19 | And the JSON at "float" should be a float 20 | And the JSON at "hash" should be a hash 21 | And the JSON at "hash" should be an object 22 | And the JSON at "integer" should be an integer 23 | And the JSON at "string" should be a string 24 | And the JSON at "true" should be a boolean 25 | -------------------------------------------------------------------------------- /gemfiles/rspec2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rspec", "~> 2.0" 6 | 7 | group :test do 8 | gem "codeclimate-test-reporter", "~> 1.0.0" 9 | gem "cucumber", "~> 1.3" 10 | end 11 | -------------------------------------------------------------------------------- /gemfiles/rspec3.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rspec", "~> 3.0" 6 | 7 | group :test do 8 | gem "codeclimate-test-reporter", "~> 1.0.0" 9 | gem "cucumber", "~> 1.3" 10 | end 11 | -------------------------------------------------------------------------------- /json_spec.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = "json_spec" 5 | gem.version = "1.1.5" 6 | 7 | gem.authors = ["Steve Richert"] 8 | gem.email = ["steve.richert@gmail.com"] 9 | gem.summary = "Easily handle JSON in RSpec and Cucumber" 10 | gem.description = "RSpec matchers and Cucumber steps for testing JSON content" 11 | gem.homepage = "https://github.com/collectiveidea/json_spec" 12 | gem.license = "MIT" 13 | 14 | gem.add_dependency "multi_json", "~> 1.0" 15 | gem.add_dependency "rspec", ">= 2.0", "< 4.0" 16 | 17 | gem.add_development_dependency "bundler", "~> 1.0" 18 | gem.add_development_dependency "rake", "~> 10.0" 19 | 20 | gem.files = `git ls-files`.split($\) 21 | gem.test_files = gem.files.grep(/^(features|spec)/) 22 | end 23 | -------------------------------------------------------------------------------- /lib/json_spec.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "rspec" 3 | require "json_spec/errors" 4 | require "json_spec/configuration" 5 | require "json_spec/exclusion" 6 | require "json_spec/helpers" 7 | require "json_spec/messages" 8 | require "json_spec/matchers" 9 | require "json_spec/memory" 10 | 11 | module JsonSpec 12 | extend Configuration 13 | extend Memory 14 | end 15 | -------------------------------------------------------------------------------- /lib/json_spec/configuration.rb: -------------------------------------------------------------------------------- 1 | require "set" 2 | 3 | module JsonSpec 4 | module Configuration 5 | DEFAULT_EXCLUDED_KEYS = %w(id created_at updated_at) 6 | 7 | attr_accessor :directory 8 | 9 | def configure(&block) 10 | instance_eval(&block) 11 | end 12 | 13 | def excluded_keys 14 | @excluded_keys ||= DEFAULT_EXCLUDED_KEYS 15 | end 16 | 17 | def excluded_keys=(keys) 18 | @excluded_keys = keys.map(&:to_s).uniq 19 | end 20 | 21 | def exclude_keys(*keys) 22 | self.excluded_keys = keys 23 | end 24 | 25 | 26 | def reset 27 | instance_variables.each { |ivar| remove_instance_variable(ivar) } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/json_spec/cucumber.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../../json_spec", __FILE__) 2 | 3 | World(JsonSpec::Helpers, JsonSpec::Matchers) 4 | 5 | After do 6 | JsonSpec.forget 7 | end 8 | 9 | When /^(?:I )?keep the (?:JSON|json)(?: response)?(?: at "(.*)")? as "(.*)"$/ do |path, key| 10 | JsonSpec.memorize(key, normalize_json(last_json, path)) 11 | end 12 | 13 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be:$/ do |path, negative, json| 14 | if negative 15 | last_json.should_not be_json_eql(JsonSpec.remember(json)).at_path(path) 16 | else 17 | last_json.should be_json_eql(JsonSpec.remember(json)).at_path(path) 18 | end 19 | end 20 | 21 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be file "(.+)"$/ do |path, negative, file_path| 22 | if negative 23 | last_json.should_not be_json_eql.to_file(file_path).at_path(path) 24 | else 25 | last_json.should be_json_eql.to_file(file_path).at_path(path) 26 | end 27 | end 28 | 29 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be (".*"|\-?\d+(?:\.\d+)?(?:[eE][\+\-]?\d+)?|\[.*\]|%?\{.*\}|true|false|null)$/ do |path, negative, value| 30 | if negative 31 | last_json.should_not be_json_eql(JsonSpec.remember(value)).at_path(path) 32 | else 33 | last_json.should be_json_eql(JsonSpec.remember(value)).at_path(path) 34 | end 35 | end 36 | 37 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include:$/ do |path, negative, json| 38 | if negative 39 | last_json.should_not include_json(JsonSpec.remember(json)).at_path(path) 40 | else 41 | last_json.should include_json(JsonSpec.remember(json)).at_path(path) 42 | end 43 | end 44 | 45 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include file "(.+)"$/ do |path, negative, file_path| 46 | if negative 47 | last_json.should_not include_json.from_file(file_path).at_path(path) 48 | else 49 | last_json.should include_json.from_file(file_path).at_path(path) 50 | end 51 | end 52 | 53 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include (".*"|\-?\d+(?:\.\d+)?(?:[eE][\+\-]?\d+)?|\[.*\]|%?\{.*\}|true|false|null)$/ do |path, negative, value| 54 | if negative 55 | last_json.should_not include_json(JsonSpec.remember(value)).at_path(path) 56 | else 57 | last_json.should include_json(JsonSpec.remember(value)).at_path(path) 58 | end 59 | end 60 | 61 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should have the following:$/ do |base, table| 62 | table.raw.each do |path, value| 63 | path = [base, path].compact.join("/") 64 | 65 | if value 66 | step %(the JSON at "#{path}" should be:), value 67 | else 68 | step %(the JSON should have "#{path}") 69 | end 70 | end 71 | end 72 | 73 | Then /^the (?:JSON|json)(?: response)? should( not)? have "(.*)"$/ do |negative, path| 74 | if negative 75 | last_json.should_not have_json_path(path) 76 | else 77 | last_json.should have_json_path(path) 78 | end 79 | end 80 | 81 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be an? (.*)$/ do |path, negative, type| 82 | if negative 83 | last_json.should_not have_json_type(type).at_path(path) 84 | else 85 | last_json.should have_json_type(type).at_path(path) 86 | end 87 | end 88 | 89 | Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? have (\d+)/ do |path, negative, size| 90 | if negative 91 | last_json.should_not have_json_size(size.to_i).at_path(path) 92 | else 93 | last_json.should have_json_size(size.to_i).at_path(path) 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/json_spec/errors.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | class Error < StandardError 3 | end 4 | 5 | class MissingPath < Error 6 | attr_reader :path 7 | 8 | def initialize(path) 9 | @path = path 10 | end 11 | 12 | def to_s 13 | %(Missing JSON path "#{path}") 14 | end 15 | end 16 | 17 | class MissingDirectory < Error 18 | def to_s 19 | "No JsonSpec.directory set" 20 | end 21 | end 22 | 23 | class MissingFile < Error 24 | attr_reader :path 25 | 26 | def initialize(path) 27 | @path = path 28 | end 29 | 30 | def to_s 31 | "No JSON file at #{path}" 32 | end 33 | end 34 | 35 | class EnumerableExpected < Error 36 | attr_reader :actual_value 37 | 38 | def initialize(actual_value) 39 | @actual_value = actual_value 40 | end 41 | 42 | def to_s 43 | "Enumerable expected, got #{actual_value.inspect}" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/json_spec/exclusion.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | module Exclusion 3 | extend self 4 | 5 | def exclude_keys(ruby) 6 | case ruby 7 | when Hash 8 | ruby.sort.inject({}) do |hash, (key, value)| 9 | hash[key] = exclude_keys(value) unless exclude_key?(key) 10 | hash 11 | end 12 | when Array 13 | ruby.map{|v| exclude_keys(v) } 14 | else ruby 15 | end 16 | end 17 | 18 | def exclude_key?(key) 19 | excluded_keys.include?(key) 20 | end 21 | 22 | def excluded_keys 23 | @excluded_keys ||= Set.new(JsonSpec.excluded_keys) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/json_spec/helpers.rb: -------------------------------------------------------------------------------- 1 | require "multi_json" 2 | 3 | module JsonSpec 4 | module Helpers 5 | extend self 6 | 7 | def parse_json(json, path = nil) 8 | ruby = MultiJson.decode("[#{json}]").first 9 | value_at_json_path(ruby, path) 10 | rescue MultiJson::DecodeError 11 | MultiJson.decode(json) 12 | end 13 | 14 | def normalize_json(json, path = nil) 15 | ruby = parse_json(json, path) 16 | generate_normalized_json(ruby) 17 | end 18 | 19 | def generate_normalized_json(ruby) 20 | case ruby 21 | when Hash, Array then JSON.pretty_generate(ruby) 22 | else ruby.to_json 23 | end 24 | end 25 | 26 | def load_json(relative_path) 27 | missing_json_directory! if JsonSpec.directory.nil? 28 | path = File.join(JsonSpec.directory, relative_path) 29 | missing_json_file!(path) unless File.exist?(path) 30 | File.read(path) 31 | end 32 | 33 | private 34 | def value_at_json_path(ruby, path) 35 | return ruby unless path 36 | 37 | path.split("/").inject(ruby) do |value, key| 38 | case value 39 | when Hash 40 | value.fetch(key){ missing_json_path!(path) } 41 | when Array 42 | missing_json_path!(path) unless key =~ /^\d+$/ 43 | value.fetch(key.to_i){ missing_json_path!(path) } 44 | else 45 | missing_json_path!(path) 46 | end 47 | end 48 | end 49 | 50 | def missing_json_path!(path) 51 | raise JsonSpec::MissingPath.new(path) 52 | end 53 | 54 | def missing_json_directory! 55 | raise JsonSpec::MissingDirectory 56 | end 57 | 58 | def missing_json_file!(path) 59 | raise JsonSpec::MissingFile.new(path) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/json_spec/matchers.rb: -------------------------------------------------------------------------------- 1 | require "json_spec/matchers/be_json_eql" 2 | require "json_spec/matchers/include_json" 3 | require "json_spec/matchers/have_json_path" 4 | require "json_spec/matchers/have_json_type" 5 | require "json_spec/matchers/have_json_size" 6 | 7 | module JsonSpec 8 | module Matchers 9 | def be_json_eql(json = nil) 10 | JsonSpec::Matchers::BeJsonEql.new(json) 11 | end 12 | 13 | def include_json(json = nil) 14 | JsonSpec::Matchers::IncludeJson.new(json) 15 | end 16 | 17 | def have_json_path(path) 18 | JsonSpec::Matchers::HaveJsonPath.new(path) 19 | end 20 | 21 | def have_json_type(type) 22 | JsonSpec::Matchers::HaveJsonType.new(type) 23 | end 24 | 25 | def have_json_size(size) 26 | JsonSpec::Matchers::HaveJsonSize.new(size) 27 | end 28 | end 29 | end 30 | 31 | RSpec.configure do |config| 32 | config.include JsonSpec::Matchers 33 | end 34 | -------------------------------------------------------------------------------- /lib/json_spec/matchers/be_json_eql.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | module Matchers 3 | class BeJsonEql 4 | include JsonSpec::Helpers 5 | include JsonSpec::Exclusion 6 | include JsonSpec::Messages 7 | 8 | attr_reader :expected, :actual 9 | 10 | def diffable? 11 | true 12 | end 13 | 14 | def initialize(expected_json = nil) 15 | @expected_json = expected_json 16 | @path = nil 17 | end 18 | 19 | def matches?(actual_json) 20 | raise "Expected equivalent JSON not provided" if @expected_json.nil? 21 | 22 | @actual, @expected = scrub(actual_json, @path), scrub(@expected_json) 23 | @actual == @expected 24 | end 25 | 26 | def at_path(path) 27 | @path = path 28 | self 29 | end 30 | 31 | def to_file(path) 32 | @expected_json = load_json(path) 33 | self 34 | end 35 | 36 | def excluding(*keys) 37 | excluded_keys.merge(keys.map(&:to_s)) 38 | self 39 | end 40 | 41 | def including(*keys) 42 | excluded_keys.subtract(keys.map(&:to_s)) 43 | self 44 | end 45 | 46 | def failure_message 47 | message_with_path("Expected equivalent JSON") 48 | end 49 | alias :failure_message_for_should :failure_message 50 | 51 | def failure_message_when_negated 52 | message_with_path("Expected inequivalent JSON") 53 | end 54 | alias :failure_message_for_should_not :failure_message_when_negated 55 | 56 | def description 57 | message_with_path("equal JSON") 58 | end 59 | 60 | private 61 | def scrub(json, path = nil) 62 | generate_normalized_json(exclude_keys(parse_json(json, path))).chomp + "\n" 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/json_spec/matchers/have_json_path.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | module Matchers 3 | class HaveJsonPath 4 | include JsonSpec::Helpers 5 | 6 | def initialize(path) 7 | @path = path 8 | end 9 | 10 | def matches?(json) 11 | parse_json(json, @path) 12 | true 13 | rescue JsonSpec::MissingPath 14 | false 15 | end 16 | 17 | def failure_message 18 | %(Expected JSON path "#{@path}") 19 | end 20 | alias :failure_message_for_should :failure_message 21 | 22 | def failure_message_when_negated 23 | %(Expected no JSON path "#{@path}") 24 | end 25 | alias :failure_message_for_should_not :failure_message_when_negated 26 | 27 | def description 28 | %(have JSON path "#{@path}") 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/json_spec/matchers/have_json_size.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | module Matchers 3 | class HaveJsonSize 4 | include JsonSpec::Helpers 5 | include JsonSpec::Messages 6 | 7 | def initialize(size) 8 | @expected = size 9 | @path = nil 10 | end 11 | 12 | def matches?(json) 13 | ruby = parse_json(json, @path) 14 | raise EnumerableExpected.new(ruby) unless Enumerable === ruby 15 | @actual = ruby.size 16 | @actual == @expected 17 | end 18 | 19 | def at_path(path) 20 | @path = path 21 | self 22 | end 23 | 24 | def failure_message 25 | message_with_path("Expected JSON value size to be #{@expected}, got #{@actual}") 26 | end 27 | alias :failure_message_for_should :failure_message 28 | 29 | def failure_message_when_negated 30 | message_with_path("Expected JSON value size to not be #{@expected}, got #{@actual}") 31 | end 32 | alias :failure_message_for_should_not :failure_message_when_negated 33 | 34 | def description 35 | message_with_path(%(have JSON size "#{@expected}")) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/json_spec/matchers/have_json_type.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | module Matchers 3 | class HaveJsonType 4 | include JsonSpec::Helpers 5 | include JsonSpec::Messages 6 | 7 | def initialize(type) 8 | @classes = type_to_classes(type) 9 | @path = nil 10 | end 11 | 12 | def matches?(json) 13 | @ruby = parse_json(json, @path) 14 | @classes.any? { |c| c === @ruby } 15 | end 16 | 17 | def at_path(path) 18 | @path = path 19 | self 20 | end 21 | 22 | def failure_message 23 | message_with_path("Expected JSON value type to be #{@classes.join(", ")}, got #{@ruby.class}") 24 | end 25 | alias :failure_message_for_should :failure_message 26 | 27 | def failure_message_when_negated 28 | message_with_path("Expected JSON value type to not be #{@classes.join(", ")}, got #{@ruby.class}") 29 | end 30 | alias :failure_message_for_should_not :failure_message_when_negated 31 | 32 | def description 33 | message_with_path(%(have JSON type "#{@classes.join(", ")}")) 34 | end 35 | 36 | private 37 | def type_to_classes(type) 38 | case type 39 | when Class then [type] 40 | when Array then type.map { |t| type_to_classes(t) }.flatten 41 | else 42 | case type.to_s.downcase 43 | when "boolean" then [TrueClass, FalseClass] 44 | when "object" then [Hash] 45 | when "nil", "null" then [NilClass] 46 | else [Module.const_get(type.to_s.capitalize)] 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/json_spec/matchers/include_json.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | module Matchers 3 | class IncludeJson 4 | include JsonSpec::Helpers 5 | include JsonSpec::Exclusion 6 | include JsonSpec::Messages 7 | 8 | def initialize(expected_json = nil) 9 | @expected_json = expected_json 10 | @path = nil 11 | end 12 | 13 | def matches?(actual_json) 14 | raise "Expected included JSON not provided" if @expected_json.nil? 15 | 16 | @actual_json = actual_json 17 | 18 | actual = parse_json(actual_json, @path) 19 | expected = exclude_keys(parse_json(@expected_json)) 20 | case actual 21 | when Hash then actual.values.map { |v| exclude_keys(v) }.include?(expected) 22 | when Array then actual.map { |e| exclude_keys(e) }.include?(expected) 23 | when String then actual.include?(expected) 24 | else false 25 | end 26 | end 27 | 28 | def at_path(path) 29 | @path = path 30 | self 31 | end 32 | 33 | def from_file(path) 34 | @expected_json = load_json(path) 35 | self 36 | end 37 | 38 | def excluding(*keys) 39 | excluded_keys.merge(keys.map(&:to_s)) 40 | self 41 | end 42 | 43 | def including(*keys) 44 | excluded_keys.subtract(keys.map(&:to_s)) 45 | self 46 | end 47 | 48 | def failure_message 49 | message_with_path("Expected #{@actual_json} to include #{@expected_json}") 50 | end 51 | alias :failure_message_for_should :failure_message 52 | 53 | def failure_message_when_negated 54 | message_with_path("Expected #{@actual_json} to not include #{@expected_json}") 55 | end 56 | alias :failure_message_for_should_not :failure_message_when_negated 57 | 58 | def description 59 | message_with_path("include JSON") 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/json_spec/memory.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | module Memory 3 | def memory 4 | @memory ||= {} 5 | end 6 | 7 | def memorize(key, value) 8 | memory[key.to_sym] = value 9 | end 10 | 11 | def remember(json) 12 | memory.empty? ? json : json % memory 13 | end 14 | 15 | def forget 16 | memory.clear 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/json_spec/messages.rb: -------------------------------------------------------------------------------- 1 | module JsonSpec 2 | module Messages 3 | def message_with_path(message) 4 | message << %( at path "#{@path}") if @path 5 | message 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/json_spec/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Configuration do 4 | it "excludes id and timestamps by default" do 5 | JsonSpec.excluded_keys.should eq ["id", "created_at", "updated_at"] 6 | end 7 | 8 | it "excludes custom keys" do 9 | JsonSpec.exclude_keys("token") 10 | JsonSpec.excluded_keys.should eq ["token"] 11 | end 12 | 13 | it "excludes custom keys via setter" do 14 | JsonSpec.excluded_keys = ["token"] 15 | JsonSpec.excluded_keys.should eq ["token"] 16 | end 17 | 18 | it "excludes custom keys via block" do 19 | JsonSpec.configure { |c| c.exclude_keys("token") } 20 | JsonSpec.excluded_keys.should eq ["token"] 21 | end 22 | 23 | it "excludes custom keys via block setter" do 24 | JsonSpec.configure { |c| c.excluded_keys = ["token"] } 25 | JsonSpec.excluded_keys.should eq ["token"] 26 | end 27 | 28 | it "excludes custom keys via instance-evaluated block" do 29 | JsonSpec.configure{ exclude_keys("token") } 30 | JsonSpec.excluded_keys.should eq ["token"] 31 | end 32 | 33 | it "ensures its excluded keys are strings" do 34 | JsonSpec.exclude_keys(:token) 35 | JsonSpec.excluded_keys.should eq ["token"] 36 | end 37 | 38 | it "ensures its excluded keys are unique" do 39 | JsonSpec.exclude_keys("token", :token) 40 | JsonSpec.excluded_keys.should eq ["token"] 41 | end 42 | 43 | it "resets its excluded keys" do 44 | original = JsonSpec.excluded_keys 45 | 46 | JsonSpec.exclude_keys("token") 47 | JsonSpec.excluded_keys.should_not eq original 48 | 49 | JsonSpec.reset 50 | JsonSpec.excluded_keys.should eq original 51 | end 52 | 53 | it "resets its directory" do 54 | JsonSpec.directory.should be_nil 55 | 56 | JsonSpec.directory = "/" 57 | JsonSpec.directory.should_not be_nil 58 | 59 | JsonSpec.reset 60 | JsonSpec.directory.should be_nil 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/json_spec/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Helpers do 4 | include described_class 5 | 6 | context "parse_json" do 7 | it "parses JSON documents" do 8 | parse_json(%({"json":["spec"]})).should eq({"json" => ["spec"]}) 9 | end 10 | 11 | it "parses JSON values" do 12 | parse_json(%("json_spec")).should eq "json_spec" 13 | end 14 | 15 | it "raises a parser error for invalid JSON" do 16 | expect{ parse_json("json_spec") }.to raise_error(MultiJson::DecodeError) 17 | end 18 | 19 | it "parses at a path if given" do 20 | json = %({"json":["spec"]}) 21 | parse_json(json, "json").should eq ["spec"] 22 | parse_json(json, "json/0").should eq "spec" 23 | end 24 | 25 | it "raises an error for a missing path" do 26 | json = %({"json":["spec"]}) 27 | %w(spec json/1).each do |path| 28 | expect{ parse_json(json, path) }.to raise_error(JsonSpec::MissingPath) 29 | end 30 | end 31 | 32 | it "parses at a numeric string path" do 33 | json = %({"1":"two"}) 34 | parse_json(json, "1").should eq "two" 35 | end 36 | end 37 | 38 | context "normalize_json" do 39 | it "normalizes a JSON document" do 40 | normalized = <<-JSON 41 | { 42 | "json": [ 43 | "spec" 44 | ] 45 | } 46 | JSON 47 | normalize_json(%({"json":["spec"]})).should eq normalized.chomp 48 | end 49 | 50 | it "normalizes at a path" do 51 | normalize_json(%({"json":["spec"]}), "json/0").should eq %("spec") 52 | end 53 | 54 | it "accepts a JSON value" do 55 | normalize_json(%("json_spec")).should eq %("json_spec") 56 | end 57 | 58 | it "normalizes JSON values" do 59 | normalize_json(%(1e+1)).should eq %(10.0) 60 | end 61 | end 62 | 63 | context "generate_normalized_json" do 64 | it "generates a normalized JSON document" do 65 | normalized = <<-JSON 66 | { 67 | "json": [ 68 | "spec" 69 | ] 70 | } 71 | JSON 72 | generate_normalized_json({"json" => ["spec"]}).should eq normalized.chomp 73 | end 74 | 75 | it "generates a normalized JSON value" do 76 | generate_normalized_json(nil).should eq %(null) 77 | end 78 | end 79 | 80 | context "load_json_file" do 81 | it "raises an error when no directory is set" do 82 | expect{ load_json("one.json") }.to raise_error(JsonSpec::MissingDirectory) 83 | end 84 | 85 | it "returns JSON when the file exists" do 86 | JsonSpec.directory = files_path 87 | load_json("one.json").should eq %({"value":"from_file"}) 88 | end 89 | 90 | it "ignores extra slashes" do 91 | JsonSpec.directory = "/#{files_path}/" 92 | load_json("one.json").should eq %({"value":"from_file"}) 93 | end 94 | 95 | it "raises an error when the file doesn't exist" do 96 | JsonSpec.directory = files_path 97 | expect{ load_json("bogus.json") }.to raise_error(JsonSpec::MissingFile) 98 | end 99 | 100 | it "raises an error when the directory doesn't exist" do 101 | JsonSpec.directory = "#{files_path}_bogus" 102 | expect{ load_json("one.json") }.to raise_error(JsonSpec::MissingFile) 103 | end 104 | 105 | it "finds nested files" do 106 | JsonSpec.directory = files_path 107 | load_json("project/one.json").should eq %({"nested":"inside_folder"}) 108 | load_json("project/version/one.json").should eq %({"nested":"deeply"}) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/json_spec/matchers/be_json_eql_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Matchers::BeJsonEql do 4 | it "matches identical JSON" do 5 | %({"json":"spec"}).should be_json_eql(%({"json":"spec"})) 6 | end 7 | 8 | it "matches differently-formatted JSON" do 9 | %({"json": "spec"}).should be_json_eql(%({"json":"spec"})) 10 | end 11 | 12 | it "matches out-of-order hashes" do 13 | %({"laser":"lemon","json":"spec"}).should be_json_eql(%({"json":"spec","laser":"lemon"})) 14 | end 15 | 16 | it "doesn't match out-of-order arrays" do 17 | %(["json","spec"]).should_not be_json_eql(%(["spec","json"])) 18 | end 19 | 20 | it "matches valid JSON values, yet invalid JSON documents" do 21 | %("json_spec").should be_json_eql(%("json_spec")) 22 | end 23 | 24 | it "matches at a path" do 25 | %({"json":["spec"]}).should be_json_eql(%("spec")).at_path("json/0") 26 | end 27 | 28 | it "ignores excluded-by-default hash keys" do 29 | JsonSpec.excluded_keys.should_not be_empty 30 | 31 | actual = expected = { "json" => "spec" } 32 | JsonSpec.excluded_keys.each { |k| actual[k] = k } 33 | actual.to_json.should be_json_eql(expected.to_json) 34 | end 35 | 36 | it "ignores custom excluded hash keys" do 37 | JsonSpec.exclude_keys("ignore") 38 | %({"json":"spec","ignore":"please"}).should be_json_eql(%({"json":"spec"})) 39 | end 40 | 41 | it "ignores nested, excluded hash keys" do 42 | JsonSpec.exclude_keys("ignore") 43 | %({"json":"spec","please":{"ignore":"this"}}).should be_json_eql(%({"json":"spec","please":{}})) 44 | end 45 | 46 | it "ignores hash keys when included in the expected value" do 47 | JsonSpec.exclude_keys("ignore") 48 | %({"json":"spec","ignore":"please"}).should be_json_eql(%({"json":"spec","ignore":"this"})) 49 | end 50 | 51 | it "doesn't match Ruby-equivalent, JSON-inequivalent values" do 52 | %({"one":1}).should_not be_json_eql(%({"one":1.0})) 53 | end 54 | 55 | it "matches different looking, JSON-equivalent values" do 56 | %({"ten":10.0}).should be_json_eql(%({"ten":1e+1})) 57 | end 58 | 59 | it "excludes extra hash keys per matcher" do 60 | JsonSpec.excluded_keys = %w(ignore) 61 | %({"id":1,"json":"spec","ignore":"please"}).should be_json_eql(%({"id":2,"json":"spec","ignore":"this"})).excluding("id") 62 | end 63 | 64 | it "excludes extra hash keys given as symbols" do 65 | JsonSpec.excluded_keys = [] 66 | %({"id":1,"json":"spec"}).should be_json_eql(%({"id":2,"json":"spec"})).excluding(:id) 67 | end 68 | 69 | it "excludes multiple keys" do 70 | JsonSpec.excluded_keys = [] 71 | %({"id":1,"json":"spec"}).should be_json_eql(%({"id":2,"json":"different"})).excluding(:id, :json) 72 | end 73 | 74 | it "includes globally-excluded hash keys per matcher" do 75 | JsonSpec.excluded_keys = %w(id ignore) 76 | %({"id":1,"json":"spec","ignore":"please"}).should_not be_json_eql(%({"id":2,"json":"spec","ignore":"this"})).including("id") 77 | end 78 | 79 | it "includes globally-included hash keys given as symbols" do 80 | JsonSpec.excluded_keys = %w(id) 81 | %({"id":1,"json":"spec"}).should_not be_json_eql(%({"id":2,"json":"spec"})).including(:id) 82 | end 83 | 84 | it "includes multiple keys" do 85 | JsonSpec.excluded_keys = %w(id json) 86 | %({"id":1,"json":"spec"}).should_not be_json_eql(%({"id":2,"json":"different"})).including(:id, :json) 87 | end 88 | 89 | it "provides a description message" do 90 | matcher = be_json_eql(%({"id":2,"json":"spec"})) 91 | matcher.matches?(%({"id":1,"json":"spec"})) 92 | matcher.description.should == "equal JSON" 93 | end 94 | 95 | it "provides a description message with path" do 96 | matcher = be_json_eql(%({"id":1,"json":["spec"]})).at_path("json/0") 97 | matcher.matches?(%({"id":1,"json":["spec"]})) 98 | matcher.description.should == %(equal JSON at path "json/0") 99 | end 100 | 101 | it "raises an error when not given expected JSON" do 102 | expect{ %({"id":1,"json":"spec"}).should be_json_eql }.to raise_error do |error| 103 | error.message.should == "Expected equivalent JSON not provided" 104 | end 105 | end 106 | 107 | it "matches file contents" do 108 | JsonSpec.directory = files_path 109 | %({ "value" : "from_file" }).should be_json_eql.to_file("one.json") 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/json_spec/matchers/have_json_path_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Matchers::HaveJsonPath do 4 | it "matches hash keys" do 5 | %({"one":{"two":{"three":4}}}).should have_json_path("one/two/three") 6 | end 7 | 8 | it "doesn't match values" do 9 | %({"one":{"two":{"three":4}}}).should_not have_json_path("one/two/three/4") 10 | end 11 | 12 | it "matches array indexes" do 13 | %([1,[1,2,[1,2,3,4]]]).should have_json_path("1/2/3") 14 | end 15 | 16 | it "respects null array values" do 17 | %([null,[null,null,[null,null,null,null]]]).should have_json_path("1/2/3") 18 | end 19 | 20 | it "matches hash keys and array indexes" do 21 | %({"one":[1,2,{"three":4}]}).should have_json_path("one/2/three") 22 | end 23 | 24 | it "provides a description message" do 25 | matcher = have_json_path("json") 26 | matcher.matches?(%({"id":1,"json":"spec"})) 27 | matcher.description.should == %(have JSON path "json") 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/json_spec/matchers/have_json_size_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Matchers::HaveJsonSize do 4 | it "counts array entries" do 5 | %([1,2,3]).should have_json_size(3) 6 | end 7 | 8 | it "counts null array entries" do 9 | %([1,null,3]).should have_json_size(3) 10 | end 11 | 12 | it "counts hash key/value pairs" do 13 | %({"one":1,"two":2,"three":3}).should have_json_size(3) 14 | end 15 | 16 | it "counts null hash values" do 17 | %({"one":1,"two":null,"three":3}).should have_json_size(3) 18 | end 19 | 20 | it "matches at a path" do 21 | %({"one":[1,2,3]}).should have_json_size(3).at_path("one") 22 | end 23 | 24 | it "provides a failure message" do 25 | matcher = have_json_size(3) 26 | matcher.matches?(%([1,2])) 27 | matcher.failure_message.should eq "Expected JSON value size to be 3, got 2" 28 | matcher.failure_message_for_should.should eq "Expected JSON value size to be 3, got 2" # RSpec 2 interface 29 | end 30 | 31 | it "provides a failure message for negation" do 32 | matcher = have_json_size(3) 33 | matcher.matches?(%([1,2,3])) 34 | matcher.failure_message_when_negated.should eq "Expected JSON value size to not be 3, got 3" 35 | matcher.failure_message_for_should_not.should eq "Expected JSON value size to not be 3, got 3" # RSpec 2 interface 36 | end 37 | 38 | it "provides a description message" do 39 | matcher = have_json_size(1) 40 | matcher.matches?(%({"id":1,"json":["spec"]})) 41 | matcher.description.should eq %(have JSON size "1") 42 | end 43 | 44 | it "provides a description message with path" do 45 | matcher = have_json_size(1).at_path("json") 46 | matcher.matches?(%({"id":1,"json":["spec"]})) 47 | matcher.description.should eq %(have JSON size "1" at path "json") 48 | end 49 | 50 | it "provides an error when parsing nil" do 51 | matcher = have_json_size(0).at_path("json") 52 | expect { matcher.matches?(%({"id":1,"json":null})) }.to raise_error(JsonSpec::EnumerableExpected, 53 | "Enumerable expected, got nil") 54 | end 55 | 56 | it "provides an error when parsing non-enumerables" do 57 | matcher = have_json_size(0).at_path("json") 58 | expect { matcher.matches?(%({"id":1,"json":12345})) }.to raise_error(JsonSpec::EnumerableExpected, 59 | "Enumerable expected, got 12345") 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/json_spec/matchers/have_json_type_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Matchers::HaveJsonType do 4 | it "matches hashes" do 5 | hash = %({}) 6 | hash.should have_json_type(Hash) 7 | hash.should have_json_type(:object) 8 | end 9 | 10 | it "matches arrays" do 11 | %([]).should have_json_type(Array) 12 | end 13 | 14 | it "matches at a path" do 15 | %({"root":[]}).should have_json_type(Array).at_path("root") 16 | end 17 | 18 | it "matches strings" do 19 | %(["json_spec"]).should have_json_type(String).at_path("0") 20 | end 21 | 22 | it "matches a valid JSON value, yet invalid JSON document" do 23 | %("json_spec").should have_json_type(String) 24 | end 25 | 26 | it "matches empty strings" do 27 | %("").should have_json_type(String) 28 | end 29 | 30 | it "matches integers" do 31 | %(10).should have_json_type(Integer) 32 | end 33 | 34 | it "matches floats" do 35 | %(10.0).should have_json_type(Float) 36 | %(1e+1).should have_json_type(Float) 37 | end 38 | 39 | it "matches booleans" do 40 | %(true).should have_json_type(:boolean) 41 | %(false).should have_json_type(:boolean) 42 | end 43 | 44 | it "matches ancestor classes" do 45 | %(10).should have_json_type(Numeric) 46 | %(10.0).should have_json_type(Numeric) 47 | end 48 | 49 | it "provides a failure message" do 50 | matcher = have_json_type(Numeric) 51 | matcher.matches?(%("foo")) 52 | matcher.failure_message.should eq "Expected JSON value type to be Numeric, got String" 53 | matcher.failure_message_for_should.should eq "Expected JSON value type to be Numeric, got String" # RSpec 2 interface 54 | end 55 | 56 | it "provides a failure message for negation" do 57 | matcher = have_json_type(Numeric) 58 | matcher.matches?(%(10.0)) 59 | 60 | matcher.failure_message_when_negated.should eq "Expected JSON value type to not be Numeric, got Float" 61 | matcher.failure_message_for_should_not.should eq "Expected JSON value type to not be Numeric, got Float" # RSpec 2 interface 62 | end 63 | 64 | it "provides a description message" do 65 | matcher = have_json_type(String) 66 | matcher.matches?(%({"id":1,"json":"spec"})) 67 | matcher.description.should eq %(have JSON type "String") 68 | end 69 | 70 | it "provides a description message with path" do 71 | matcher = have_json_type(String).at_path("json") 72 | matcher.matches?(%({"id":1,"json":"spec"})) 73 | matcher.description.should eq %(have JSON type "String" at path "json") 74 | end 75 | 76 | context "somewhat uselessly" do 77 | it "matches true" do 78 | %(true).should have_json_type(TrueClass) 79 | end 80 | 81 | it "matches false" do 82 | %(false).should have_json_type(FalseClass) 83 | end 84 | 85 | it "matches null" do 86 | null = %(null) 87 | null.should have_json_type(NilClass) 88 | null.should have_json_type(:nil) 89 | null.should have_json_type(:null) 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/json_spec/matchers/include_json_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Matchers::IncludeJson do 4 | it "matches included array elements" do 5 | json = %(["one",1,1.0,true,false,null]) 6 | json.should include_json(%("one")) 7 | json.should include_json(%(1)) 8 | json.should include_json(%(1.0)) 9 | json.should include_json(%(true)) 10 | json.should include_json(%(false)) 11 | json.should include_json(%(null)) 12 | end 13 | 14 | it "matches an array included in an array" do 15 | json = %([[1,2,3],[4,5,6]]) 16 | json.should include_json(%([1,2,3])) 17 | json.should include_json(%([4,5,6])) 18 | end 19 | 20 | it "matches a hash included in an array" do 21 | json = %([{"one":1},{"two":2}]) 22 | json.should include_json(%({"one":1})) 23 | json.should include_json(%({"two":2})) 24 | end 25 | 26 | it "matches included hash values" do 27 | json = %({"string":"one","integer":1,"float":1.0,"true":true,"false":false,"null":null}) 28 | json.should include_json(%("one")) 29 | json.should include_json(%(1)) 30 | json.should include_json(%(1.0)) 31 | json.should include_json(%(true)) 32 | json.should include_json(%(false)) 33 | json.should include_json(%(null)) 34 | end 35 | 36 | it "matches a hash included in a hash" do 37 | json = %({"one":{"two":3},"four":{"five":6}}) 38 | json.should include_json(%({"two":3})) 39 | json.should include_json(%({"five":6})) 40 | end 41 | 42 | it "matches an array included in a hash" do 43 | json = %({"one":[2,3],"four":[5,6]}) 44 | json.should include_json(%([2,3])) 45 | json.should include_json(%([5,6])) 46 | end 47 | 48 | it "matches a substring" do 49 | json = %("json") 50 | json.should include_json(%("js")) 51 | json.should include_json(%("json")) 52 | end 53 | 54 | it "matches at a path" do 55 | %({"one":{"two":[3,4]}}).should include_json(%([3,4])).at_path("one") 56 | end 57 | 58 | it "ignores excluded keys" do 59 | %([{"id":1,"two":3}]).should include_json(%({"two":3})) 60 | end 61 | 62 | it "provides a description message" do 63 | matcher = include_json(%({"json":"spec"})) 64 | matcher.matches?(%({"id":1,"json":"spec"})) 65 | matcher.description.should == "include JSON" 66 | end 67 | 68 | it "provides a description message with path" do 69 | matcher = include_json(%("spec")).at_path("json/0") 70 | matcher.matches?(%({"id":1,"json":["spec"]})) 71 | matcher.description.should == %(include JSON at path "json/0") 72 | end 73 | 74 | it "provides a useful failure message for should" do 75 | matcher = include_json(%([4,5,6])) 76 | matcher.matches?(%([1,2,3])) 77 | matcher.failure_message_for_should.should == "Expected [1,2,3] to include [4,5,6]" 78 | end 79 | 80 | it "provides a useful failure message for should not" do 81 | matcher = include_json(%(3)) 82 | matcher.matches?(%([1,2,3])) 83 | matcher.failure_message_for_should_not.should == "Expected [1,2,3] to not include 3" 84 | end 85 | 86 | it "raises an error when not given expected JSON" do 87 | expect{ %([{"id":1,"two":3}]).should include_json }.to raise_error do |error| 88 | error.message.should == "Expected included JSON not provided" 89 | end 90 | end 91 | 92 | it "matches file contents" do 93 | JsonSpec.directory = files_path 94 | %({"one":{"value":"from_file"},"four":{"five":6}}).should include_json.from_file("one.json") 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/json_spec/matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Matchers do 4 | let(:environment) do 5 | klass = Class.new 6 | klass.send(:include, JsonSpec::Matchers) 7 | klass.new 8 | end 9 | 10 | let(:json){ %({"json":"spec"}) } 11 | 12 | context "be_json_eql" do 13 | it "instantiates its matcher" do 14 | JsonSpec::Matchers::BeJsonEql.should_receive(:new).with(json) 15 | environment.be_json_eql(json) 16 | end 17 | 18 | it "returns its matcher" do 19 | matcher = environment.be_json_eql(json) 20 | matcher.should be_a(JsonSpec::Matchers::BeJsonEql) 21 | end 22 | end 23 | 24 | context "include_json" do 25 | it "instantiates its matcher" do 26 | JsonSpec::Matchers::IncludeJson.should_receive(:new).with(json) 27 | environment.include_json(json) 28 | end 29 | 30 | it "returns its matcher" do 31 | matcher = environment.include_json(json) 32 | matcher.should be_a(JsonSpec::Matchers::IncludeJson) 33 | end 34 | end 35 | 36 | context "have_json_path" do 37 | let(:path){ "json" } 38 | 39 | it "instantiates its matcher" do 40 | JsonSpec::Matchers::HaveJsonPath.should_receive(:new).with(path) 41 | environment.have_json_path(path) 42 | end 43 | 44 | it "returns its matcher" do 45 | matcher = environment.have_json_path(path) 46 | matcher.should be_a(JsonSpec::Matchers::HaveJsonPath) 47 | end 48 | end 49 | 50 | context "have_json_type" do 51 | let(:type){ Hash } 52 | 53 | it "instantiates its matcher" do 54 | JsonSpec::Matchers::HaveJsonType.should_receive(:new).with(type) 55 | environment.have_json_type(type) 56 | end 57 | 58 | it "returns its matcher" do 59 | matcher = environment.have_json_type(type) 60 | matcher.should be_a(JsonSpec::Matchers::HaveJsonType) 61 | end 62 | end 63 | 64 | context "have_json_size" do 65 | let(:size){ 1 } 66 | 67 | it "instantiates its matcher" do 68 | JsonSpec::Matchers::HaveJsonSize.should_receive(:new).with(size) 69 | environment.have_json_size(size) 70 | end 71 | 72 | it "returns its matcher" do 73 | matcher = environment.have_json_size(size) 74 | matcher.should be_a(JsonSpec::Matchers::HaveJsonSize) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/json_spec/memory_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe JsonSpec::Memory do 4 | it "has a memory" do 5 | JsonSpec.memory.should == {} 6 | end 7 | 8 | it "memorizes strings" do 9 | JsonSpec.memorize(:key, "value") 10 | JsonSpec.memory.should == { key: "value" } 11 | end 12 | 13 | it "symbolizes keys" do 14 | JsonSpec.memorize("key", "value") 15 | JsonSpec.memory.should == { key: "value" } 16 | end 17 | 18 | it "regurgitates unremembered strings" do 19 | JsonSpec.remember("foo%{bar}").should == "foo%{bar}" 20 | end 21 | 22 | it "remembers strings" do 23 | JsonSpec.memorize(:bar, "baz") 24 | JsonSpec.remember("foo%{bar}").should == "foobaz" 25 | end 26 | 27 | it "forgets" do 28 | JsonSpec.memorize(:key, "value") 29 | JsonSpec.forget 30 | JsonSpec.memory.should == {} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV["CODECLIMATE_REPO_TOKEN"] 2 | require "simplecov" 3 | SimpleCov.start 4 | end 5 | 6 | require "json_spec" 7 | 8 | RSpec.configure do |config| 9 | config.before do 10 | JsonSpec.reset 11 | end 12 | 13 | config.expect_with :rspec do |c| 14 | c.syntax = [:should, :expect] 15 | end 16 | config.mock_with :rspec do |c| 17 | c.syntax = [:should, :expect] 18 | end 19 | end 20 | 21 | def files_path 22 | File.expand_path("../support/files", __FILE__) 23 | end 24 | -------------------------------------------------------------------------------- /spec/support/files/one.json: -------------------------------------------------------------------------------- 1 | {"value":"from_file"} -------------------------------------------------------------------------------- /spec/support/files/project/one.json: -------------------------------------------------------------------------------- 1 | {"nested":"inside_folder"} -------------------------------------------------------------------------------- /spec/support/files/project/two.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": null, 3 | "one": [ 4 | 1 5 | ], 6 | "three": [ 7 | 1, 8 | 2, 9 | 3 10 | ], 11 | "two": [ 12 | 1, 13 | 2 14 | ], 15 | "zero": [ 16 | 17 | ] 18 | } -------------------------------------------------------------------------------- /spec/support/files/project/version/one.json: -------------------------------------------------------------------------------- 1 | {"nested":"deeply"} -------------------------------------------------------------------------------- /spec/support/files/project/version/two.json: -------------------------------------------------------------------------------- 1 | { 2 | "json": "spec" 3 | } -------------------------------------------------------------------------------- /spec/support/files/two.json: -------------------------------------------------------------------------------- 1 | { 2 | "array": [ 3 | "json", 4 | "spec" 5 | ], 6 | "created_at": "2011-07-08 02:27:34", 7 | "empty_array": [ 8 | 9 | ], 10 | "empty_hash": { 11 | }, 12 | "false": false, 13 | "float": 10.0, 14 | "hash": { 15 | "json": "spec" 16 | }, 17 | "id": 1, 18 | "integer": 10, 19 | "negative": -10, 20 | "null": null, 21 | "string": "json_spec", 22 | "true": true, 23 | "updated_at": "2011-07-08 02:28:50" 24 | } --------------------------------------------------------------------------------