rspec_json_matchers 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +61 -0
- data/lib/rspec_json_matchers/absence_matcher.rb +30 -0
- data/lib/rspec_json_matchers/api_response_matcher.rb +76 -0
- data/lib/rspec_json_matchers/fuzzy_matcher.rb +210 -0
- data/lib/rspec_json_matchers/json_matcher_definition.rb +34 -0
- data/lib/rspec_json_matchers.rb +81 -0
- data/spec/json_matchers/absence_matcher_objects.rb +3 -0
- data/spec/json_matchers/rspec_json_matchers_objects.rb +16 -0
- data/spec/lib/rspec_json_matchers/absence_matcher_spec.rb +16 -0
- data/spec/lib/rspec_json_matchers_spec.rb +36 -0
- data/spec/spec_helper.rb +7 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f8195ff8f9a4378643145f3199e37b00edca07a3
|
4
|
+
data.tar.gz: 60c5a6ba469eeb3b29084580d4b283031e9948fb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f7750332e4a7083e58fa3afe934f322947a43c2167d5cfbbc48e18b5ae5f7cfddf309be0c0a598d441684a21b665f11b7fb21a077fec444f30d1e9363f1d79cf
|
7
|
+
data.tar.gz: 52489d95dbd36bee0bf2e44a82266e4d62c01960552cbd45aa654541a1feac2a486d53a19e86a4845aa0cd58b1e39087d9dc8d1df54331b27fe6ae517c9ecee1
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Brigade
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# rspec_json_matchers
|
2
|
+
|
3
|
+
Are you tired of unreadable output while verifying your API's JSON output? What
|
4
|
+
about duplicating and losing track of your JSON structure definitions?
|
5
|
+
`rspec_json_matchers` provides declarative JSON structure definitions and
|
6
|
+
matchers with clear error output.
|
7
|
+
|
8
|
+
## How to use
|
9
|
+
|
10
|
+
### Add it to your `Gemfile`
|
11
|
+
```ruby
|
12
|
+
gem 'rspec_json_matchers'
|
13
|
+
```
|
14
|
+
|
15
|
+
### Add config `spec/spec_helper.rb`
|
16
|
+
```ruby
|
17
|
+
RSpec.configure do |config|
|
18
|
+
config.include RSpecJsonMatchers
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
Optionally if you choose to define your matchers in a their own directory,
|
23
|
+
import them explicitly like this:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
Dir[Rails.root.join('spec/json_matchers/**/*.rb')].each { |f| require f }
|
27
|
+
```
|
28
|
+
|
29
|
+
### Define your matchers
|
30
|
+
```ruby
|
31
|
+
RSpecJsonMatchers.define_api_matcher :json_object do
|
32
|
+
strings { an_instance_of(String) }
|
33
|
+
integers { a_kind_of(Integer) }
|
34
|
+
actual_values { 10 }
|
35
|
+
keys_that_should_not_exist { absent }
|
36
|
+
booleans { a_boolean_value }
|
37
|
+
nilable { a_nil_value.or(a_kind_of(Integer)) }
|
38
|
+
another_object { a_serialized_other_json_object }
|
39
|
+
|
40
|
+
nested_structure do
|
41
|
+
match_api_response(
|
42
|
+
foo: an_instance_of(String),
|
43
|
+
bar: 'baz'
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
Your matchers will be available as `a_serialized_object_name` and can be used
|
50
|
+
through `match_api_response(a_serialized_object_name)` or
|
51
|
+
`be_a_serialized_object_name`
|
52
|
+
|
53
|
+
|
54
|
+
### Write your test!
|
55
|
+
|
56
|
+
See the `spec/` folder for an example
|
57
|
+
|
58
|
+
|
59
|
+
## Authors
|
60
|
+
* Ryan Fitzgerald [rf-]
|
61
|
+
* Hao Su [haosu]
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RSpecJsonMatchers
|
2
|
+
# This is a special matcher that always returns false under normal
|
3
|
+
# circumstances, used to indicate keys that should not be present in a given
|
4
|
+
# API response.
|
5
|
+
#
|
6
|
+
# The only circumstance where the matcher may return true is in the class
|
7
|
+
# method `is_an_absence_matcher?`, which takes a matcher and feeds a special
|
8
|
+
# `ABSENCE_MARKER` value into it which should fail every possible matcher
|
9
|
+
# other than AbsenceMatcher, which it passes. We can use this to determine
|
10
|
+
# whether it's OK for a given key to be absent from a response. TODO: try to
|
11
|
+
# make this explanation less confusing.
|
12
|
+
class AbsenceMatcher < RSpec::Matchers::BuiltIn::Equal
|
13
|
+
ABSENCE_MARKER = Object.new
|
14
|
+
|
15
|
+
# Test whether the given matcher is either an instance of AbsenceMatcher or
|
16
|
+
# a compound matcher containing an AbsenceMatcher, like
|
17
|
+
# 'absent.or(a_kind_of(Integer))'.
|
18
|
+
def self.is_an_absence_matcher?(matcher)
|
19
|
+
matcher === ABSENCE_MARKER && !(matcher === Object.new)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@expected = ABSENCE_MARKER
|
24
|
+
end
|
25
|
+
|
26
|
+
def description
|
27
|
+
'absent'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module RSpecJsonMatchers
|
2
|
+
# General matcher for API responses, used to compose matchers for the overall
|
3
|
+
# API response or individual model serializations.
|
4
|
+
class ApiResponseMatcher < RSpec::Matchers::BuiltIn::BaseMatcher
|
5
|
+
attr_reader :results_with_errors
|
6
|
+
|
7
|
+
def initialize(expected,
|
8
|
+
object_definition: nil,
|
9
|
+
object_name: nil,
|
10
|
+
context: nil)
|
11
|
+
@expected = expected
|
12
|
+
@object_definition = object_definition
|
13
|
+
@object_name = object_name
|
14
|
+
@context = context
|
15
|
+
end
|
16
|
+
|
17
|
+
# Use our implementation of FuzzyMatcher to test the given data against our
|
18
|
+
# expected data.
|
19
|
+
# @param expected [Object] Expected values or matchers
|
20
|
+
# @param actual [Object] Actual data to be validated
|
21
|
+
# @return [Boolean]
|
22
|
+
def matches?(actual)
|
23
|
+
@actual = actual
|
24
|
+
|
25
|
+
if @object_definition
|
26
|
+
combined = @expected.reverse_merge @object_definition.to_hash(@context)
|
27
|
+
end
|
28
|
+
|
29
|
+
@did_match, @results_with_errors =
|
30
|
+
FuzzyMatcher.match_values(combined || @expected, @actual)
|
31
|
+
@did_match
|
32
|
+
end
|
33
|
+
|
34
|
+
# Overrides `failure_message` to return our own field specific failure
|
35
|
+
# messsages with color!
|
36
|
+
# @return [String]
|
37
|
+
def failure_message
|
38
|
+
"Expected API response to match specification:\n#{pretty_results}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Overrides `failure_message_when_negated` when testing that an API response
|
42
|
+
# does not include a resource.
|
43
|
+
# @return [String]
|
44
|
+
def failure_message_when_negated
|
45
|
+
"Expected API response not to match specification, but it did:\n#{pretty_results}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Formats our results object with PP and, resetting the color at the
|
49
|
+
# beginning of each line to override RSpec's default coloring.
|
50
|
+
# @return [String]
|
51
|
+
def pretty_results
|
52
|
+
@results_with_errors.pretty_inspect.gsub(/^/, "\e[0m")
|
53
|
+
end
|
54
|
+
|
55
|
+
# If we have explicitly defined the type of API matcher we want, report
|
56
|
+
# that, otherwise describe the general API matcher
|
57
|
+
# @return [String]
|
58
|
+
def description
|
59
|
+
description = surface_descriptions_in(@expected).inspect
|
60
|
+
|
61
|
+
if @object_name
|
62
|
+
"a serialized #{@object_name}" \
|
63
|
+
"#{" matching #{description}" if description != '{}'}"
|
64
|
+
else
|
65
|
+
"an api response matching #{description}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Overridden because we don't want to inspect `@object_definition` and
|
70
|
+
# `@context`
|
71
|
+
# @return [String]
|
72
|
+
def inspect
|
73
|
+
"#<ApiResponseMatcher#{" for #{@object_name}" if @object_name}>"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
module RSpecJsonMatchers
|
2
|
+
# This is a fork of `RSpec::Support::FuzzyMatcher`, which is a tool used to
|
3
|
+
# recursively match a given structure (of arrays, hashes, and other objects)
|
4
|
+
# against an expected structure. The expected structure can contain scalar
|
5
|
+
# values or RSpec matchers; leaves are compared using the `===` operator.
|
6
|
+
#
|
7
|
+
# The differences between this module and the RSpec version are:
|
8
|
+
# * Instead of implementing predicate methods that return true or false (and
|
9
|
+
# bail out of the recursion as soon as they notice a problem), our methods
|
10
|
+
# return both a success value and a representation of the structure that
|
11
|
+
# includes failure messages. That lets us show exactly where the structures
|
12
|
+
# differed in a way that's more reliable than textual diffing.
|
13
|
+
# * We support `absent` (`RSpecJsonMatchers::AbsenceMatcher`) as a placeholder
|
14
|
+
# that means "this key should not exist in the hash we're matching
|
15
|
+
# against".
|
16
|
+
module FuzzyMatcher
|
17
|
+
extend RSpec::Matchers::Composable # for surface_descriptions_in
|
18
|
+
|
19
|
+
# Simple wrapper for formatting individual errors in a failure message.
|
20
|
+
class FailureDescription
|
21
|
+
# @param message [String] Message to be rendered.
|
22
|
+
def initialize(message)
|
23
|
+
@message = message
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String] the given message, with extra formatting and red
|
27
|
+
# highlighting.
|
28
|
+
def inspect
|
29
|
+
"\e[31m(FAILURE: #{@message})\e[0m"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Helper used to match individual values against each other. If the values
|
34
|
+
# are arrays or hashes, we delegate to match_arrays or match_hashes.
|
35
|
+
# @param expected [Object] expected values or matchers
|
36
|
+
# @param actual [Object] actual data
|
37
|
+
# @return [Array(Boolean, Object)] a tuple whose first member is a Boolean
|
38
|
+
# representing whether the match succeeded and whose second member is
|
39
|
+
# something which, when `pretty_inspect` is called on it, will return a
|
40
|
+
# string representing the original value where the match succeeded or
|
41
|
+
# describing the failure where the match failed.
|
42
|
+
def self.match_values(expected, actual)
|
43
|
+
if Array === expected && Enumerable === actual
|
44
|
+
return match_arrays(expected, actual.to_a)
|
45
|
+
end
|
46
|
+
|
47
|
+
if Hash === expected && Hash === actual
|
48
|
+
return match_hashes(expected, actual)
|
49
|
+
end
|
50
|
+
|
51
|
+
begin
|
52
|
+
did_match = (actual == expected || expected === actual)
|
53
|
+
rescue ArgumentError
|
54
|
+
# Some objects, like 0-arg lambdas on 1.9+, raise
|
55
|
+
# ArgumentError for `expected === actual`.
|
56
|
+
false
|
57
|
+
end
|
58
|
+
|
59
|
+
if did_match
|
60
|
+
[true, actual]
|
61
|
+
else
|
62
|
+
[false, extract_results_with_errors(expected, actual)]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Helper used to match arrays against each other.
|
67
|
+
# @param expected_list [Array] expected values or matchers
|
68
|
+
# @param actual_list [Array] actual data
|
69
|
+
# @return [Array(Boolean, Array)] a tuple whose first member is a
|
70
|
+
# Boolean representing whether the match succeeded and whose second
|
71
|
+
# member is something which, when `pretty_inspect` is called on it, will
|
72
|
+
# return a string representing the original value where the match
|
73
|
+
# succeeded or describing the failure where the match failed.
|
74
|
+
def self.match_arrays(expected_list, actual_list)
|
75
|
+
all_matched = true
|
76
|
+
result_list = []
|
77
|
+
|
78
|
+
# For indexes that are present in both lists, match the values against
|
79
|
+
# their respective expectations.
|
80
|
+
expected_list.take(actual_list.length).each_with_index do |expected, idx|
|
81
|
+
actual = actual_list[idx]
|
82
|
+
value_matched, value = match_values(expected, actual)
|
83
|
+
all_matched &&= value_matched
|
84
|
+
result_list << value
|
85
|
+
end
|
86
|
+
|
87
|
+
# If the expected list was longer, add "was absent" errors.
|
88
|
+
expected_list.drop(actual_list.length).each do |expected|
|
89
|
+
all_matched = false
|
90
|
+
result_list << failed_match_message(expected, 'absent')
|
91
|
+
end
|
92
|
+
|
93
|
+
# If the actual list was longer, add "should have been absent" errors.
|
94
|
+
actual_list.drop(expected_list.length).each do |actual|
|
95
|
+
all_matched = false
|
96
|
+
result_list << extra_key_message(actual.inspect)
|
97
|
+
end
|
98
|
+
|
99
|
+
[all_matched, result_list]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Helper used to match hashes against each other. Also checks for the
|
103
|
+
# existence of unexpected keys and the absence of expected keys.
|
104
|
+
# @param expected_hash [Hash] expected values or matchers
|
105
|
+
# @param actual_hash [Hash] actual data
|
106
|
+
# @return [Array(Boolean, Hash)] a tuple whose first member is a Boolean
|
107
|
+
# representing whether the match succeeded and whose second member is
|
108
|
+
# something which, when `pretty_inspect` is called on it, will return a
|
109
|
+
# string representing the original value where the match succeeded or
|
110
|
+
# describing the failure where the match failed.
|
111
|
+
def self.match_hashes(expected_hash, actual_hash)
|
112
|
+
all_matched = true
|
113
|
+
result_hash = {}
|
114
|
+
|
115
|
+
# Stringify expected keys so that we can use new-style hashes in our
|
116
|
+
# specs.
|
117
|
+
expected_hash = expected_hash.stringify_keys
|
118
|
+
|
119
|
+
# Errors for missing keys, or extra keys are created at the hash level
|
120
|
+
# because it is difficult to detect at the element level whether or not a
|
121
|
+
# `nil` value is caused by a missing key, or an actual `nil` value.
|
122
|
+
expected_hash.each do |expected_key, expected_value|
|
123
|
+
if actual_hash.key?(expected_key)
|
124
|
+
actual_value = actual_hash[expected_key]
|
125
|
+
value_matched, value = match_values(expected_value, actual_value)
|
126
|
+
all_matched &&= value_matched
|
127
|
+
elsif AbsenceMatcher.is_an_absence_matcher?(expected_value)
|
128
|
+
# We expected the value to not be present, and it isn't, so we're all
|
129
|
+
# good.
|
130
|
+
else
|
131
|
+
# Mark any missing keys as failures
|
132
|
+
value = failed_match_message(expected_value, 'absent')
|
133
|
+
all_matched = false
|
134
|
+
end
|
135
|
+
|
136
|
+
result_hash[expected_key] = value
|
137
|
+
end
|
138
|
+
|
139
|
+
# If there are extra keys, we should mark them as invalid
|
140
|
+
(actual_hash.keys - expected_hash.keys).each do |key|
|
141
|
+
result_hash[key] = extra_key_message(actual_hash[key].inspect)
|
142
|
+
all_matched = false
|
143
|
+
end
|
144
|
+
|
145
|
+
[all_matched, result_hash]
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.extract_results_with_errors(expected, actual)
|
149
|
+
if expected.respond_to?(:results_with_errors)
|
150
|
+
return expected.results_with_errors
|
151
|
+
end
|
152
|
+
|
153
|
+
case expected
|
154
|
+
when RSpec::Matchers::BuiltIn::Compound::Or
|
155
|
+
extract_results_from_or_matcher(expected, actual)
|
156
|
+
when RSpec::Matchers::BuiltIn::All
|
157
|
+
extract_results_from_all_matcher(expected, actual)
|
158
|
+
else
|
159
|
+
failed_match_message(expected, actual.inspect)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Check both branches for structured results before falling back to the
|
164
|
+
# matcher's description method.
|
165
|
+
def self.extract_results_from_or_matcher(expected, actual)
|
166
|
+
[expected.matcher_1, expected.matcher_2].each do |matcher|
|
167
|
+
result = extract_results_with_errors(matcher, actual)
|
168
|
+
return result unless result.is_a?(FailureDescription)
|
169
|
+
end
|
170
|
+
|
171
|
+
failed_match_message(expected, actual.inspect)
|
172
|
+
end
|
173
|
+
|
174
|
+
# To deal with the `all` matcher, we need to rerun the matcher on each
|
175
|
+
# element so that we can then try to extract results from it.
|
176
|
+
def self.extract_results_from_all_matcher(expected, actual)
|
177
|
+
unless actual.respond_to?(:map)
|
178
|
+
return failed_match_message(expected, actual.inspect)
|
179
|
+
end
|
180
|
+
|
181
|
+
actual.map do |actual_item|
|
182
|
+
cloned_matcher = expected.matcher.clone
|
183
|
+
matches = cloned_matcher.matches?(actual_item)
|
184
|
+
|
185
|
+
if matches
|
186
|
+
actual_item
|
187
|
+
else
|
188
|
+
extract_results_with_errors(cloned_matcher, actual_item)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Generate a failure description based on expected and actual values.
|
194
|
+
# @param expected [Object] expected matcher or value
|
195
|
+
# @param actual [Object] actual value
|
196
|
+
# @return [FailureDescription]
|
197
|
+
def self.failed_match_message(expected, actual)
|
198
|
+
description = surface_descriptions_in(expected).inspect
|
199
|
+
FailureDescription.new("was #{actual}, should have been #{description}")
|
200
|
+
end
|
201
|
+
|
202
|
+
# Generate a failure description for a key that should not have been
|
203
|
+
# present.
|
204
|
+
# @param actual_value [Object]
|
205
|
+
# @return [FailureDescription]
|
206
|
+
def self.extra_key_message(actual_value)
|
207
|
+
FailureDescription.new("was #{actual_value}, should have been absent")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RSpecJsonMatchers
|
2
|
+
# Helper class that powers our DSL for defining API matchers.
|
3
|
+
class JsonMatcherDefinition < BasicObject
|
4
|
+
def initialize
|
5
|
+
@attrs = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
# Defines a matcher for the specified field.
|
9
|
+
# @param method_name [Symbol] Name of the field to match against
|
10
|
+
# @block Block which, when called, returns matcher used to verify actual
|
11
|
+
# data
|
12
|
+
# @example
|
13
|
+
# id { a_kind_of Integer } # the id attribute should be a number
|
14
|
+
# rubocop:disable Style/MethodMissing
|
15
|
+
# disabled since method existence can depend on args / block
|
16
|
+
def method_missing(name, *args, &block)
|
17
|
+
if args.length > 0 || block.nil?
|
18
|
+
super
|
19
|
+
else
|
20
|
+
@attrs[name.to_s] = block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
# rubocop:enable Style/MethodMissing
|
24
|
+
|
25
|
+
# @param matcher_context [Object] The context in which to run the matcher
|
26
|
+
# procs. This should be the spec instance.
|
27
|
+
# @return [Hash] of field name, matcher pairs to be used for validation
|
28
|
+
def to_hash(matcher_context)
|
29
|
+
::Hash[@attrs.map do |name, matcher_proc|
|
30
|
+
[name, matcher_context.instance_exec(&matcher_proc)]
|
31
|
+
end]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rspec_json_matchers/json_matcher_definition'
|
2
|
+
require 'rspec_json_matchers/api_response_matcher'
|
3
|
+
require 'rspec_json_matchers/fuzzy_matcher'
|
4
|
+
require 'rspec_json_matchers/absence_matcher'
|
5
|
+
|
6
|
+
require 'active_support/core_ext/hash/reverse_merge'
|
7
|
+
require 'active_support/core_ext/hash/keys'
|
8
|
+
|
9
|
+
# This module defines matchers that are useful in writing request specs for our
|
10
|
+
# API. Specifically, they make it easy to specify the structure of the entire
|
11
|
+
# response, taking advantage of the fact that our API is mostly made up of
|
12
|
+
# objects that should be consistent in their structure anywhere that they
|
13
|
+
# appear.
|
14
|
+
#
|
15
|
+
# For example, a "profile" is a type of API object that has the same structure
|
16
|
+
# whether it's appearing in the author field of a comment or the profile field
|
17
|
+
# of the /me endpoint. We can use the `define_api_matcher` method to define
|
18
|
+
# matchers called `a_serialized_profile` and `be_a_serialized_profile` which
|
19
|
+
# will match successfully against any serialized profile. If the user passes a
|
20
|
+
# hash into the matcher, they can customize it by specifying specific values it
|
21
|
+
# should include or using any RSpec matcher to narrow down the range of
|
22
|
+
# acceptable values for a given field. They can also use the `absent`
|
23
|
+
# matcher to assert that a given key should *not* be included in the response.
|
24
|
+
#
|
25
|
+
# @see spec/api_matchers/profile.rb for an example of an API matcher.
|
26
|
+
module RSpecJsonMatchers
|
27
|
+
# @param [Symbol]
|
28
|
+
# @yield [] declarations for expected fields and matcher pairs
|
29
|
+
def self.define_api_matcher(name, &block)
|
30
|
+
definition = JsonMatcherDefinition.new
|
31
|
+
definition.instance_eval(&block)
|
32
|
+
|
33
|
+
# @param [expected] a hash of values/matchers with which to customize this
|
34
|
+
# API matcher instance.
|
35
|
+
# @return [ApiResponseMatcher] a matcher based on the provided name,
|
36
|
+
# definition block, and hash of expected values/matchers.
|
37
|
+
define_method "a_serialized_#{name}" do |expected = {}|
|
38
|
+
ApiResponseMatcher.new(
|
39
|
+
expected,
|
40
|
+
object_definition: definition,
|
41
|
+
object_name: name.to_s,
|
42
|
+
context: self
|
43
|
+
)
|
44
|
+
end
|
45
|
+
alias_method "be_a_serialized_#{name}", "a_serialized_#{name}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# This matcher validates all or part of an API response against a schema. If
|
49
|
+
# the response violates the schema, it shows which parts of the response
|
50
|
+
# failed to match our expectations.
|
51
|
+
# @param expected [Hash] a hash representing an API response. Its values can
|
52
|
+
# be matchers (like `an_instance_of(String)`) or concrete values (like
|
53
|
+
# `10`).
|
54
|
+
# @return [ApiResponseMatcher]
|
55
|
+
def match_api_response(expected)
|
56
|
+
ApiResponseMatcher.new(expected)
|
57
|
+
end
|
58
|
+
alias_method 'a_hash_matching_api_response', 'match_api_response'
|
59
|
+
|
60
|
+
# This matcher works like `match_api_response`, but also verifies that
|
61
|
+
# response matches `meta: a_serialized_pagination_metadata_hash`. This
|
62
|
+
# prevents that line from needing to be repeated in every spec of a paginated
|
63
|
+
# endpoint.
|
64
|
+
def match_paginated_api_response(expected)
|
65
|
+
match_api_response(
|
66
|
+
expected.merge(meta: a_serialized_pagination_metadata_hash)
|
67
|
+
)
|
68
|
+
end
|
69
|
+
alias_method 'a_hash_matching_paginated_api_response',
|
70
|
+
'match_paginated_api_response'
|
71
|
+
|
72
|
+
# @return [AbsenceMatcher] a matcher indicating that a key shouldn't be
|
73
|
+
# present in an API response. Only useful in combination with
|
74
|
+
# ApiResponseMatcher.
|
75
|
+
def absent
|
76
|
+
AbsenceMatcher.new
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
# Dir[Rails.root.join('spec/api_matchers/**/*.rb')].each { |f| require f }
|
@@ -0,0 +1,16 @@
|
|
1
|
+
puts :doing_this
|
2
|
+
|
3
|
+
RSpecJsonMatchers.define_api_matcher :matcher_test_object do
|
4
|
+
numeric_type { a_kind_of(Integer) }
|
5
|
+
string_type { an_instance_of(String) }
|
6
|
+
numeric_value { numeric_value }
|
7
|
+
string_value { string_value }
|
8
|
+
absent_value { absent }
|
9
|
+
nil_value { a_nil_value }
|
10
|
+
composed_matcher { a_nil_value.or(an_instance_of(String)) }
|
11
|
+
another_matcher { a_serialized_matcher_another_object }
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpecJsonMatchers.define_api_matcher :matcher_another_object do
|
15
|
+
foo { an_instance_of(String) }
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe RSpecJsonMatchers::AbsenceMatcher do
|
4
|
+
# See `spec/json_matchers/absence_matcher_objects.rb` for matcher definitions
|
5
|
+
|
6
|
+
let(:valid_hash) { {} }
|
7
|
+
let(:invalid_hash) { { absent: :not_absent } }
|
8
|
+
|
9
|
+
it 'matches absent keys' do
|
10
|
+
expect(valid_hash).to match_api_response(a_serialized_absent)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'detects a non-absent key' do
|
14
|
+
expect(invalid_hash).to_not be_a_serialized_absent
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe RSpecJsonMatchers do
|
4
|
+
# See `spec/json_matchers/rspec_json_matchers_objects.rb` for matcher
|
5
|
+
# definitions
|
6
|
+
|
7
|
+
let(:string_value) { 'bar' }
|
8
|
+
let(:numeric_value) { 2 }
|
9
|
+
|
10
|
+
let(:valid_hash) do
|
11
|
+
{
|
12
|
+
'numeric_type' => 1,
|
13
|
+
'string_type' => 'foo',
|
14
|
+
'numeric_value' => numeric_value,
|
15
|
+
'string_value' => string_value,
|
16
|
+
# absent_value
|
17
|
+
'nil_value' => nil,
|
18
|
+
'composed_matcher' => 'not nil but can be nil',
|
19
|
+
'another_matcher' => {
|
20
|
+
'foo' => 'bar'
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:serialized_another_object) { { 'foo' => 'bar' } }
|
26
|
+
|
27
|
+
let(:invalid_hash) { { absent: :not_absent } }
|
28
|
+
|
29
|
+
it 'matches absent keys' do
|
30
|
+
expect(valid_hash).to match_api_response(a_serialized_matcher_test_object)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'detects a non-absent key' do
|
34
|
+
expect(invalid_hash).to_not be_a_serialized_matcher_test_object
|
35
|
+
end
|
36
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec_json_matchers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Fitzgerald
|
8
|
+
- Hao Su
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2017-01-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '3.0'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '3.0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: activesupport
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '4.0'
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 4.0.2
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - "~>"
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '4.0'
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 4.0.2
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rspec
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activesupport
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.0'
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 4.0.2
|
72
|
+
type: :runtime
|
73
|
+
prerelease: false
|
74
|
+
version_requirements: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - "~>"
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '4.0'
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 4.0.2
|
82
|
+
description: JSON matchers for RSpec
|
83
|
+
email:
|
84
|
+
- rwfitzge@gmail.com
|
85
|
+
- me@haosu.org
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- Gemfile
|
91
|
+
- LICENSE
|
92
|
+
- README.md
|
93
|
+
- lib/rspec_json_matchers.rb
|
94
|
+
- lib/rspec_json_matchers/absence_matcher.rb
|
95
|
+
- lib/rspec_json_matchers/api_response_matcher.rb
|
96
|
+
- lib/rspec_json_matchers/fuzzy_matcher.rb
|
97
|
+
- lib/rspec_json_matchers/json_matcher_definition.rb
|
98
|
+
- spec/json_matchers/absence_matcher_objects.rb
|
99
|
+
- spec/json_matchers/rspec_json_matchers_objects.rb
|
100
|
+
- spec/lib/rspec_json_matchers/absence_matcher_spec.rb
|
101
|
+
- spec/lib/rspec_json_matchers_spec.rb
|
102
|
+
- spec/spec_helper.rb
|
103
|
+
homepage: https://github.com/brigade/rspec_json_matchers/
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.5.1
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: RSpec JSON Matchers
|
127
|
+
test_files: []
|