rspec-json_matchers 0.1.0.alpha.1 → 0.1.0.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: addd43fea47a6a22fa215a491070570ff1fa9f76
4
- data.tar.gz: 54e450f71ec0af5594bc660f73bb9a3918356a89
3
+ metadata.gz: c8a8d3fdb38106995f0116c157f8a02a915d97de
4
+ data.tar.gz: d11e9f4fcc661dbb02731398bf306f79304728e6
5
5
  SHA512:
6
- metadata.gz: b761496f1884102fb0a0e79b93d75bbbb935c176fbf1eb83942c823678791b021ffbd376c403f56185933740cae6dc7a8820628f47cb9a25330c1d04c8370595
7
- data.tar.gz: 88a84df10ffe3bd711b92fa801147ff49abe9f35f4d9be9b6319f1cbc2991755b2150d9211d7899262c5839e6c48d31c3723b820c46e1de1f3383ad86c370f75
6
+ metadata.gz: 7b80c90b32ebd1caabda67697d3922c9085ab8fe680705fc77257bebdfffb6b046e48d9f23eefe0d6b4c31361e4215f4f9d35d1a27c10459ecd07a926e5e3862
7
+ data.tar.gz: cf832440789cd1f53e82c83bd32446519c2acd6e285da092962a96358676458716f5505a078da5bed0e0bbceb9fc77059aba61f8d3c601735b35218b37c98794
data/.gitignore CHANGED
@@ -3,7 +3,6 @@
3
3
  /Gemfile.lock
4
4
  /_yardoc/
5
5
  /coverage/
6
- /doc/
7
6
  /pkg/
8
7
  /spec/reports/
9
8
  /tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ Style/StringLiterals:
2
+ EnforcedStyle: double_quotes
3
+
4
+ Style/TrailingComma:
5
+ # If EnforcedStyleForMultiline is comma, the cop requires a comma after the
6
+ # last item of a list, but only for lists where each item is on its own line.
7
+ # If EnforcedStyleForMultiline is consistent_comma, the cop requires a comma
8
+ # after the last item of a list, for all lists.
9
+ EnforcedStyleForMultiline: comma
10
+
11
+ Style/SignalException:
12
+ EnforcedStyle: only_fail
13
+
14
+ # Multi-line method chaining should be done with leading dots.
15
+ Style/DotPosition:
16
+ EnforcedStyle: trailing
17
+
18
+ Style/MultilineOperationIndentation:
19
+ EnforcedStyle: indented
20
+
21
+ Style/AlignParameters:
22
+ EnforcedStyle: with_fixed_indentation
23
+
24
+ Style/ClosingParenthesisIndentation:
25
+ Enabled: false
26
+
27
+ Style/FileName:
28
+ Enabled: false
29
+
30
+ Style/PredicateName:
31
+ Enabled: false
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in rspec-json_matchers.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -18,6 +18,8 @@ This gem provides a collection of RSpec matchers for testing JSON data.
18
18
  It aims to make JSON testing flexible & easier, especially for testing multiple properties.
19
19
  It does not and will not have anything related to JSON Schema.
20
20
 
21
+ You can read [the story of this project](https://github.com/PikachuEXE/rspec-json_matchers/blob/master/doc/Story.md) if you have time.
22
+
21
23
  ## Installation
22
24
 
23
25
  Add this line to your application's Gemfile:
@@ -402,6 +404,30 @@ specify do
402
404
  end # => pass
403
405
 
404
406
 
407
+ # `NullableOf` is an expectation that works like `AnyOf`
408
+ # Except it always passes when the subject is `nil`
409
+ specify do
410
+ expect({a: 1}.to_json).to be_json.
411
+ with_content(a: expectations::NullableOf[1])
412
+ end # => pass
413
+ specify do
414
+ expect({a: 1}.to_json).to be_json.
415
+ with_content(a: expectations::NullableOf[0, 1, 2])
416
+ end # => pass
417
+ specify do
418
+ expect({a: 1}.to_json).to be_json.
419
+ with_content(a: expectations::NullableOf[false, expectations::Anything, false])
420
+ end # => pass
421
+ specify do
422
+ expect({a: 1}.to_json).to be_json.
423
+ with_content(a: expectations::NullableOf[false, false, false])
424
+ end # => fail
425
+ specify do
426
+ expect({a: nil}.to_json).to be_json.
427
+ with_content(a: expectations::NullableOf[false, false, false])
428
+ end # => fail
429
+
430
+
405
431
  # `AnyOf` is an expectation that passes when **any** of "expectations" passed in
406
432
  # "expects" the subject
407
433
  # It will convert non `Expectation` objects into `Expectation` objects,
@@ -488,6 +514,16 @@ there is no such matcher.
488
514
  Just use `be_json.with_content` with classes.
489
515
 
490
516
 
517
+ ## Pitfalls
518
+
519
+ ### Error message colorized output in RubyMine
520
+
521
+ Add something like
522
+ `-rawesome_print -e "AwesomePrint.defaults={plain: true}"` to `Ruby arguments`
523
+ for `Run/Debug Configurations => Defaults => RSpec`
524
+ That way you could keep the color when running `rspec` from console
525
+
526
+
491
527
  ## Contributing
492
528
 
493
529
  1. Fork it ( https://github.com/PikachuEXE/rspec-json_matchers/fork )
data/Rakefile CHANGED
@@ -10,5 +10,5 @@ if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"]
10
10
  sh "appraisal install && rake appraisal spec"
11
11
  end
12
12
  else
13
- task :default => :spec
13
+ task default: :spec
14
14
  end
data/doc/Story.md ADDED
@@ -0,0 +1,129 @@
1
+ # Story of the project
2
+ This file describes the motive of building this project, alternatives and the future.
3
+
4
+ ## Alternative projects that had been used before
5
+ The author of this project does his job as a developer mainly on a Ruby on Rails project, which contains endpoints returning responses with JSON data. And he uses RSpec (only) to write tests for those endpoints.
6
+
7
+ ### `json_spec`
8
+
9
+ #### Usage
10
+ At first he was using [`json_spec`](https://github.com/collectiveidea/json_spec), which was good enough for a endpoints with limited number of properties (e.g. an `object` representing a "resource"). The main matcher methods used was:
11
+ - `have_json_path` (but actually not used that often)
12
+ - `have_json_type`
13
+ - `have_json_size`
14
+
15
+ #### Issues
16
+ However, when trying to create examples to verify the actual values of properties, he was unable to find a matcher method for it. Instead he has to use a "helper method" `parse_json` to parse JSON string into a Ruby `Hash` and extract the value from the sometimes deep-nested `Hash`.
17
+
18
+ Besides, when number of properties increases, it started to become annoying to duplicate & modify the examples for each of properties that requires testing.
19
+
20
+ ### `airborne`
21
+
22
+ #### Usage
23
+ To solve the two issues above, [`airborne`](https://github.com/brooklynDev/airborne) is used. It covers the previous project's features:
24
+ - `expect_json_types` => `have_json_type`
25
+ - `expect_json_keys` => `have_json_path`
26
+ - `expect_json_sizes` => `have_json_size`
27
+
28
+ It also solve the two issues of previous project:
29
+ - It has an additional method `expect_json`
30
+ This solves the lack of way of verifying the actual values of properties
31
+ - It allows (and maybe only accepts) `Hash` style of expectation
32
+ And also accepts an optional "path" to avoid writing deep-nested `Hash` in examples
33
+ This solves the need to duplicate examples to verify multiple properties of an `object`.
34
+
35
+ It has slightly more flexible for type matching, with the following options (copied from its README):
36
+ * `:int` or `:integer`
37
+ * `:float`
38
+ * `:bool` or `:boolean`
39
+ * `:string`
40
+ * `:date`
41
+ * `:object`
42
+ * `:null`
43
+ * `:array`
44
+ * `:array_of_integers` or `:array_of_ints`
45
+ * `:array_of_floats`
46
+ * `:array_of_strings`
47
+ * `:array_of_booleans` or `:array_of_bools`
48
+ * `:array_of_objects`
49
+ * `:array_of_arrays`
50
+ If the properties are optional and may not appear in the response, you can append `_or_null` to the types above.
51
+
52
+ #### Issues
53
+ Sometimes when the symbol was misspelled, a strange error would be raised (seems fixed in recent versions).
54
+
55
+ Also `:null` was only added recently, which indicates another issue: the lack of possibility to extend the project without raising Pull Requests. This issue is properly not very serious when the project is actively maintained and still easy enough to add changes to it quickly & cleanly (without monkey-patching).
56
+
57
+
58
+ ### Other Project
59
+ To solve the issues of `airborne`, the author first found other gems, but there are other issues:
60
+ #### [`rspec-json_matcher`](https://github.com/r7kamura/rspec-json_matcher)
61
+
62
+ - It lacks the ability to verify the following things easily
63
+ - number elements of `array`
64
+ - the object type/value with logic "or"
65
+
66
+ It is possible with the use of `Proc` since the project use `#===` and `Proc#===` is similar to `Proc#call`) but not quite "easy" (requires much typing)
67
+ - The method focusing non built-in "expectation" could lead to silent false positive results, since the definition of `#===` is unclear, it was only used to allow:
68
+ - `Regexp`
69
+ - `Proc`
70
+ - `Class`
71
+ - `Range` (not even mentioned in its README)
72
+
73
+ There are other classes that define `#===` but with different meanings, remembering/checking the definition of `#===` of classes of custom objects before putting those object as "expectations" (to ensure no false positive result) is not convenient and easy to be forgotten.
74
+
75
+ #### [`match_json`](https://github.com/WhitePayments/match_json)
76
+ - Required to type JSON String as expectations
77
+ - Lack of ability to verify
78
+ - the number of elements of `array`
79
+ - the data type
80
+
81
+ It was discovered during the development of this project.
82
+
83
+ #### [`json-matchers`](https://github.com/seanpdoyle/json-matchers)
84
+ It only supports [JSON Schema](http://json-schema.org/).
85
+ Using JSON Schema to validate JSON lacks the ability to verify the value of property exactly for all data types.
86
+ Also JSON Schema is not a well known standard and lacks stability (the homepage said it's still a draft)
87
+
88
+ #### [`json-schema`](https://github.com/ruby-json-schema/json-schema)
89
+ Same as `json-matchers`
90
+
91
+
92
+ ## This Project
93
+
94
+ ### Objectives
95
+ - To implement as many features from previous projects as possible
96
+ - To solve most issues found in previous projects
97
+
98
+ ### Development Path
99
+ - First this project was developed following the pattern of `rspec-json_matcher`, as it has most things required for this project. You can see matcher & comparer classes in this project.
100
+ - "Path" support was added to support have the feature provided by `json_spec` & `airborne`
101
+ - "Expectation" classes was added to remove the usage of `#===` following the pattern "contract" classes in [`contracts.ruby`](https://github.com/egonSchiele/contracts.ruby) since the author is a user of that project.
102
+ - Refactor without the usage of external tool (yet)
103
+ - Start using [`appraisal`](https://github.com/thoughtbot/appraisal) to test this gem against all `rspec` versions that should be supported. And that did discover a few errors with `rspec` `3.0`.
104
+ - Prepare config file for [Travis](https://travis-ci.org/) (copy from other existing objects)
105
+ - Create the first commit (finally)
106
+ - Setup related services:
107
+ - [Travis CI](https://travis-ci.org/) to ensure the spec is passed
108
+ - [Gemnasium](https://gemnasium.com/) to ensure the updated dependencies
109
+ - [Code Climate](https://codeclimate.com/) to ensure high code quality
110
+ - [Coveralls](https://coveralls.io/) to ensure high test coverage
111
+ - [Inch CI](https://inch-ci.org/) to ensure inline doc with quality
112
+ - [Gitter](https://gitter.im/) for a free chat room for the project
113
+ - Add badges to README
114
+ - Refactor according to result from Code Climate & [rubocop](https://github.com/bbatsov/rubocop)
115
+ - Improve inline doc according to result from running [inch](https://github.com/rrrene/inch) locally
116
+ - Release an "alpha" version and actually it in real project
117
+
118
+
119
+ # Future
120
+ In the near future:
121
+ - Release a "beta" version for public testing and feedback
122
+ - React according to feedback before the official version release, if any
123
+ - Add `CONTRIBUTING.md` if appropriate
124
+
125
+ There are several features that might be considered to be implemented in the future:
126
+ - Built-in "expectation" for "type or null"
127
+ - Built-in "expectation" for "date"
128
+ - Path matching feature in [`airborne`](https://github.com/brooklynDev/airborne)
129
+ - `be_json.with_types` which only accepts classes (only? not sure)
@@ -1 +1 @@
1
- require_relative "rspec/json_matchers"
1
+ require_relative "rspec/json_matchers"
@@ -1,4 +1,5 @@
1
1
  require "abstract_class"
2
+ require "forwardable"
2
3
  require "set"
3
4
  require_relative "../expectation"
4
5
  require_relative "comparison_result"
@@ -14,24 +15,28 @@ module RSpec
14
15
  # The subclasses only need to implement the behaviour of matching keys
15
16
  # when both expected & actual are same type of collection
16
17
  class AbstractComparer
17
- attr_reader *[
18
+ attr_reader(*[
18
19
  :actual,
19
20
  :expected,
20
21
  :reasons,
21
22
  :value_matching_proc,
22
- ]
23
+ ])
23
24
 
24
- # Creates a comparer that actually use the {value_matching_proc} for matching {#actual} and {#expected}
25
+ # Creates a comparer that actually use the {value_matching_proc}
26
+ # for matching {#actual} and {#expected}
25
27
  # This class is respossible to aggregating
26
28
  # the matching result for each element of {#expected},
27
29
  # and compare the keys/indices as well
28
30
  #
29
- # @param actual [Object] the actual value
30
- # @param expected [Object] the expected value
31
+ # @param actual [Object]
32
+ # the actual "thing", should be an {Enumerable}
33
+ # @param expected [Object]
34
+ # the expected "thing", should be an {Enumerable}
31
35
  # @param reasons [Array<String>]
32
36
  # failure reasons, mostly the path parts
33
37
  # @param value_matching_proc [Proc]
34
- # the proc that actually compares the expected & actual and returns a boolean
38
+ # the proc that actually compares
39
+ # the expected & actual and returns a boolean
35
40
  def initialize(actual, expected, reasons, value_matching_proc)
36
41
  @actual = actual
37
42
  @expected = expected
@@ -43,9 +48,7 @@ module RSpec
43
48
  # @return [Boolean]
44
49
  # `true` if #actual & #expected are the same
45
50
  def compare
46
- if has_matched_value?
47
- return ComparisonResult.new(true, reasons)
48
- end
51
+ return ComparisonResult.new(true, reasons) if has_matched_value?
49
52
 
50
53
  has_matched_collection?
51
54
  end
@@ -74,16 +77,14 @@ module RSpec
74
77
 
75
78
  # @note with side effect on `#reasons`
76
79
  def has_matched_keys?
77
- raise NotImplementedError
80
+ fail NotImplementedError
78
81
  end
79
82
 
80
83
  # @note with side effect on `#reasons`
81
84
  def has_matched_values?
82
- comparison_result = {
83
- Array => HasMatchedArrayValues,
84
- Hash => HasMatchedHashValues,
85
- }.fetch(expected.class).
86
- new(expected, actual, reasons, value_matching_proc, self.class).
85
+ comparison_result = EXPECTED_VALUE_CLASS_TO_EXPECTATION_MAPPING.
86
+ fetch(expected.class).
87
+ new(self).
87
88
  comparison_result
88
89
 
89
90
  comparison_result.matched?.tap do |matched|
@@ -91,96 +92,91 @@ module RSpec
91
92
  end
92
93
  end
93
94
 
95
+ def actual_keys
96
+ @actual_keys ||= Utils::CollectionKeysExtractor.extract(actual)
97
+ end
98
+
99
+ def expected_keys
100
+ @expected_keys ||= Utils::CollectionKeysExtractor.extract(expected)
101
+ end
102
+
103
+ # Represents an "expectation" for matching all elements
104
+ # in {#actual} & {#expected}
94
105
  class HasMatchedValues
95
106
  extend AbstractClass
107
+ extend Forwardable
96
108
 
97
- private
98
- attr_reader *[
99
- :actual,
100
- :expected,
101
- :reasons,
102
- :value_matching_proc,
103
-
104
- :comparer_class,
105
- ]
106
109
  public
107
110
 
108
- # Create a "matching" operation object that can return a {Comparers::ComparisonResult}
111
+ # Create a "matching" operation object
112
+ # that can return a {Comparers::ComparisonResult}
109
113
  #
110
- # @param expected [Object] the expected "thing", should be an {Enumerable}
111
- # @param actual [Object] the actual "thing", should be an {Enumerable}
112
- # @param reasons [Array<String>]
113
- # failure reasons, mostly the path parts
114
- # @param value_matching_proc [Proc]
115
- # the proc that actually compares the expected & actual and returns a boolean
116
- # @param comparer_class [Class<AbstractComparer>]
117
- # the class that should be used recursively
118
- def initialize(expected, actual, reasons, value_matching_proc, comparer_class)
119
- @actual = actual
120
- @expected = expected
121
- @reasons = reasons
122
-
123
- @value_matching_proc = value_matching_proc
124
-
125
- @comparer_class = comparer_class
114
+ # @param comparer [AbstractComparer]
115
+ # the comparer that creates this object, for fetching values
116
+ def initialize(comparer)
117
+ @comparer = comparer
126
118
  end
127
119
 
128
120
  def comparison_result
129
121
  each_element_enumerator.each do |element|
130
- comparison_result = has_matched_value_class.new(
131
- element,
132
- expected,
133
- actual,
134
- reasons,
135
- value_matching_proc,
136
- comparer_class,
137
- ).comparison_result
122
+ result = comparison_result_for_element(element)
138
123
 
139
- return comparison_result unless comparison_result.matched?
124
+ return result unless result.matched?
140
125
  end
141
126
 
142
127
  Comparers::ComparisonResult.new(true, reasons)
143
128
  end
144
129
 
145
130
  def each_element_enumerator
146
- raise NotImplementedError
131
+ fail NotImplementedError
147
132
  end
148
133
 
149
134
  def has_matched_value_class
150
- raise NotImplementedError
135
+ fail NotImplementedError
151
136
  end
152
137
 
153
- class HasMatchedValue
154
- extend AbstractClass
138
+ private
155
139
 
156
- private
157
- attr_reader *[
158
- :element,
140
+ def_delegators(*[
141
+ :comparer,
142
+ :expected,
143
+ :reasons,
144
+ ])
159
145
 
160
- :actual,
161
- :expected,
162
- :reasons,
146
+ attr_reader(*[
147
+ :comparer,
148
+ ])
163
149
 
164
- :value_matching_proc,
150
+ def comparer_class
151
+ comparer.class
152
+ end
153
+
154
+ def comparison_result_for_element(element)
155
+ has_matched_value_class.
156
+ new(
157
+ element,
158
+ comparer,
159
+ ).comparison_result
160
+ end
161
+
162
+ # Represents an "expectation" for matching one element
163
+ # in {#actual} & {#expected}
164
+ class HasMatchedValue
165
+ extend AbstractClass
166
+ extend Forwardable
165
167
 
166
- :comparer_class,
167
- ]
168
168
  public
169
169
 
170
- # Create a "matching" operation object that can return a {Comparers::ComparisonResult}
170
+ # Create a "matching" operation object
171
+ # that can return a {Comparers::ComparisonResult}
171
172
  # Unlike {HasMatchedValues}, this is for an element of `expected`
172
173
  #
173
- # @param element [Integer, String, Symbol] a index/key from expected (not value)
174
+ # @param element [Integer, String, Symbol]
175
+ # a index/key from expected (not value)
174
176
  # @param (see HasMatchedValues#initialize)
175
- def initialize(element, expected, actual, reasons, value_matching_proc, comparer_class)
177
+ def initialize(element, comparer)
176
178
  @element = element
177
- @actual = actual
178
- @expected = expected
179
- @reasons = reasons
180
-
181
- @value_matching_proc = value_matching_proc
182
-
183
- @comparer_class = comparer_class
179
+ @comparer = comparer
184
180
  end
185
181
 
186
182
  def comparison_result
@@ -194,6 +190,23 @@ module RSpec
194
190
 
195
191
  private
196
192
 
193
+ attr_reader(*[
194
+ :element,
195
+ :comparer,
196
+ ])
197
+
198
+ def_delegators(*[
199
+ :comparer,
200
+ :expected,
201
+ :actual,
202
+ :reasons,
203
+ :value_matching_proc,
204
+ ])
205
+
206
+ def comparer_class
207
+ comparer.class
208
+ end
209
+
197
210
  def result
198
211
  @result ||= comparer_class.
199
212
  new(
@@ -206,22 +219,25 @@ module RSpec
206
219
  end
207
220
 
208
221
  def actual_contain_element?
209
- raise NotImplementedError
222
+ fail NotImplementedError
210
223
  end
211
224
 
212
225
  def actual_for_element
213
- raise NotImplementedError
226
+ fail NotImplementedError
214
227
  end
228
+
215
229
  def expected_for_element
216
- raise NotImplementedError
230
+ fail NotImplementedError
217
231
  end
218
232
 
219
233
  def reason
220
- raise NotImplementedError
234
+ fail NotImplementedError
221
235
  end
222
236
  end
223
237
  end
224
238
 
239
+ # (see HasMatchedValues)
240
+ # {#expected} should be {Array}
225
241
  class HasMatchedArrayValues < HasMatchedValues
226
242
  def each_element_enumerator
227
243
  expected.each_index
@@ -231,9 +247,13 @@ module RSpec
231
247
  HasMatchedArrayValue
232
248
  end
233
249
 
250
+ # (see HasMatchedValues::HasMatchedValue)
251
+ # {#expected} should be {Array}
234
252
  class HasMatchedArrayValue < HasMatchedValues::HasMatchedValue
235
253
  private
254
+
236
255
  alias_method :index, :element
256
+
237
257
  public
238
258
 
239
259
  def actual_contain_element?
@@ -243,6 +263,7 @@ module RSpec
243
263
  def actual_for_element
244
264
  actual[index]
245
265
  end
266
+
246
267
  def expected_for_element
247
268
  expected[index]
248
269
  end
@@ -253,6 +274,8 @@ module RSpec
253
274
  end
254
275
  end
255
276
 
277
+ # (see HasMatchedValues)
278
+ # {#expected} should be {Hash}
256
279
  class HasMatchedHashValues < HasMatchedValues
257
280
  def each_element_enumerator
258
281
  expected.each_key
@@ -262,9 +285,13 @@ module RSpec
262
285
  HasMatchedHashValue
263
286
  end
264
287
 
288
+ # (see HasMatchedValues::HasMatchedValue)
289
+ # {#expected} should be {Array}
265
290
  class HasMatchedHashValue < HasMatchedValues::HasMatchedValue
266
291
  private
292
+
267
293
  alias_method :key, :element
294
+
268
295
  public
269
296
 
270
297
  def actual_contain_element?
@@ -274,6 +301,7 @@ module RSpec
274
301
  def actual_for_element
275
302
  actual[key.to_s]
276
303
  end
304
+
277
305
  def expected_for_element
278
306
  expected[key]
279
307
  end
@@ -283,6 +311,12 @@ module RSpec
283
311
  end
284
312
  end
285
313
  end
314
+
315
+ EXPECTED_VALUE_CLASS_TO_EXPECTATION_MAPPING = {
316
+ Array => HasMatchedArrayValues,
317
+ Hash => HasMatchedHashValues,
318
+ }.freeze
319
+ private_constant :EXPECTED_VALUE_CLASS_TO_EXPECTATION_MAPPING
286
320
  end
287
321
  end
288
322
  end