Skip to content

Commit 0ec8b69

Browse files
committed
Add match_json for exact matching of JSON
This commit adds a `match_json` matcher which only succeeds when the value being tested has *exactly* the fields specified. The work here was partly based on waterlink#39.
1 parent f15f853 commit 0ec8b69

File tree

3 files changed

+170
-5
lines changed

3 files changed

+170
-5
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
Feature: match_json matcher
2+
3+
As a developer writing expectations against JSON with RSpec
4+
I want to be able to match against the exact structure of a JSON object
5+
So I have confidence no additional fields are included
6+
7+
Background:
8+
Given a file "spec/spec_helper.rb" with:
9+
"""ruby
10+
require "rspec/json_expectations"
11+
"""
12+
And a local "SIMPLE_HASH" with:
13+
"""ruby
14+
{
15+
id: 25,
16+
17+
name: "John"
18+
}
19+
"""
20+
And a local "SIMPLE_JSON" with:
21+
"""json
22+
{
23+
"name": "Matthew",
24+
"age": 33
25+
}
26+
"""
27+
And a local "NESTED_JSON" with:
28+
"""json
29+
{
30+
"nested": {
31+
"a": 1,
32+
"b": 2
33+
}
34+
}
35+
"""
36+
37+
38+
39+
Scenario: Correctly expecting hash to include simple json
40+
Given a file "spec/simple_example_spec.rb" with:
41+
"""ruby
42+
require "spec_helper"
43+
44+
RSpec.describe "A json response" do
45+
subject { %{SIMPLE_HASH} }
46+
47+
it "has basic info about user" do
48+
expect(subject).to match_json(
49+
id: 25,
50+
51+
name: "John"
52+
)
53+
end
54+
end
55+
"""
56+
When I run "rspec spec/simple_example_spec.rb"
57+
Then I see:
58+
"""
59+
1 example, 0 failures
60+
"""
61+
62+
Scenario: Wrongly expecting hash to include simple json
63+
Given a file "spec/simple_with_fail_spec.rb" with:
64+
"""ruby
65+
require "spec_helper"
66+
67+
RSpec.describe "A json response" do
68+
subject { %{SIMPLE_HASH} }
69+
70+
it "has basic info about user" do
71+
expect(subject).to match_json(
72+
id: 25,
73+
74+
)
75+
end
76+
end
77+
"""
78+
When I run "rspec spec/simple_with_fail_spec.rb"
79+
Then I see:
80+
"""
81+
1 example, 1 failure
82+
"""
83+
And I see:
84+
"""
85+
expected: {id: 25, email: "[email protected]"}
86+
got: {id: 25, email: "[email protected]", name: "John"}
87+
"""
88+
89+
Scenario: Wrongly expecting JSON string to include simple JSON
90+
Given a file "spec/json_string_with_fail_spec.rb" with:
91+
"""ruby
92+
require "spec_helper"
93+
94+
RSpec.describe "test" do
95+
subject { '%{SIMPLE_JSON}' }
96+
97+
it "passes" do
98+
expect(subject).to match_json(name: "Matthew")
99+
end
100+
end
101+
"""
102+
When I run "rspec spec/json_string_with_fail_spec.rb"
103+
Then I see:
104+
"""
105+
1 example, 1 failure
106+
"""
107+
And I see:
108+
"""
109+
expected: {name: "Matthew"}
110+
got: {"name" => "Matthew", "age" => 33}
111+
"""
112+
113+
Scenario: Wrongly expecting JSON string to include nested JSON
114+
Given a file "spec/fail_in_nested_json.rb" with:
115+
"""ruby
116+
require "spec_helper"
117+
118+
RSpec.describe "test" do
119+
subject { '%{NESTED_JSON}' }
120+
121+
it "passes" do
122+
expect(subject).to match_json(nested: {a: 1})
123+
end
124+
end
125+
"""
126+
When I run "rspec spec/fail_in_nested_json.rb"
127+
Then I see:
128+
"""
129+
1 example, 1 failure
130+
"""
131+
And I see:
132+
"""
133+
json atom at path "nested" is not equal to expected value:
134+
"""
135+
And I see:
136+
"""
137+
expected: {a: 1}
138+
got: {"a" => 1, "b" => 2}
139+
"""

lib/rspec/json_expectations/json_traverser.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class JsonTraverser
1616
class << self
1717
def traverse(errors, expected, actual, negate=false, prefix=[], options={})
1818
[
19-
handle_hash(errors, expected, actual, negate, prefix),
19+
handle_hash(errors, expected, actual, negate, prefix, options),
2020
handle_array(errors, expected, actual, negate, prefix),
2121
handle_unordered(errors, expected, actual, negate, prefix, options),
2222
handle_value(errors, expected, actual, negate, prefix),
@@ -28,22 +28,27 @@ def traverse(errors, expected, actual, negate=false, prefix=[], options={})
2828

2929
private
3030

31-
def handle_keyvalue(errors, expected, actual, negate=false, prefix=[])
31+
def handle_keyvalue(errors, expected, actual, negate=false, prefix=[], options={})
32+
if options[:exact_size] && expected.size != actual.size
33+
errors[prefix.join("/")] = {expected:, actual:}
34+
return conditionally_negate(false, negate)
35+
end
36+
3237
expected.map do |key, value|
3338
new_prefix = prefix + [key]
3439
if has_key?(actual, key)
35-
traverse(errors, value, fetch(actual, key), negate, new_prefix)
40+
traverse(errors, value, fetch(actual, key), negate, new_prefix, options)
3641
else
3742
errors[new_prefix.join("/")] = :no_key unless negate
3843
conditionally_negate(false, negate)
3944
end
4045
end.all? || false
4146
end
4247

43-
def handle_hash(errors, expected, actual, negate=false, prefix=[])
48+
def handle_hash(errors, expected, actual, negate=false, prefix=[], options={})
4449
return nil unless expected.is_a?(Hash)
4550

46-
handle_keyvalue(errors, expected, actual, negate, prefix)
51+
handle_keyvalue(errors, expected, actual, negate, prefix, options)
4752
end
4853

4954
def handle_array(errors, expected, actual, negate=false, prefix=[])

lib/rspec/json_expectations/matchers.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ def traverse(expected, actual, negate=false)
2121
end
2222
end
2323

24+
RSpec::JsonExpectations::MatcherFactory.new(:match_json).define_matcher do
25+
def traverse(expected, actual, negate=false)
26+
supported_types = [Hash, Array, ::RSpec::JsonExpectations::Matchers::UnorderedArrayMatcher]
27+
28+
unless supported_types.any? { expected.is_a?(it) }
29+
raise ArgumentError, "Expected value in match_json must be JSON-ish"
30+
end
31+
32+
representation = actual.is_a?(String) ? JSON.parse(actual) : actual
33+
34+
RSpec::JsonExpectations::JsonTraverser.traverse(
35+
@include_json_errors = { _negate: negate},
36+
expected,
37+
representation,
38+
negate,
39+
[],
40+
{exact_size: true}
41+
)
42+
end
43+
end
44+
2445
RSpec::JsonExpectations::MatcherFactory.new(:include_unordered_json).define_matcher do
2546
def traverse(expected, actual, negate=false)
2647
unless expected.is_a?(Array)

0 commit comments

Comments
 (0)