json_spectacular 1.0.0
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/.gitignore +22 -0
- data/.rspec +2 -0
- data/.rubocop.yml +40 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +5 -0
- data/Guardfile +37 -0
- data/LICENSE.txt +22 -0
- data/README.md +89 -0
- data/Rakefile +3 -0
- data/json_spectacular.gemspec +30 -0
- data/lib/json_spectacular.rb +6 -0
- data/lib/json_spectacular/diff_descriptions.rb +141 -0
- data/lib/json_spectacular/matcher.rb +67 -0
- data/lib/json_spectacular/rspec.rb +38 -0
- data/lib/json_spectacular/version.rb +5 -0
- data/spec/eql_json_spec.rb +396 -0
- data/spec/spec_helper.rb +7 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: aa60f6a80d45be14af65ac6a7d9098745fc554a8
|
4
|
+
data.tar.gz: 5084d6e054706dc3a3f0b85175a0df65c6b4c38f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 486c82eddc6cfad05df2d4643163c117ebf23abf7e7de09b28aadbfb17c476708f5abee7f44c2dca7652a78f6bf90b37126cb6de16228a68e39c802c025efe59
|
7
|
+
data.tar.gz: 701b3e8789ca02a87176461e31a6c54cb80bf304992045593156da4e4235d776c45d6fac2e1704a458e9bf1e73207f0a0a3ab73ac1de73c0897bd8e930f41dfd
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.5
|
3
|
+
|
4
|
+
Metrics/LineLength:
|
5
|
+
Max: 120
|
6
|
+
|
7
|
+
Metrics/AbcSize:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Style/Documentation:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Style/ClassAndModuleChildren:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Style/RescueModifier:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Layout/MultilineOperationIndentation:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Layout/TrailingWhitespace:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Metrics/PerceivedComplexity:
|
26
|
+
Max: 20
|
27
|
+
|
28
|
+
Metrics/CyclomaticComplexity:
|
29
|
+
Max: 20
|
30
|
+
|
31
|
+
Metrics/MethodLength:
|
32
|
+
CountComments: false
|
33
|
+
Max: 20
|
34
|
+
|
35
|
+
Metrics/BlockLength:
|
36
|
+
ExcludedMethods:
|
37
|
+
- describe
|
38
|
+
- context
|
39
|
+
- it
|
40
|
+
- let
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
- README LICENSE
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
4
|
+
require 'guard/rspec/dsl'
|
5
|
+
|
6
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
7
|
+
|
8
|
+
last_run_spec = nil
|
9
|
+
|
10
|
+
watch(%r{^lib/(.+)\.rb$}) do |match|
|
11
|
+
file_path =
|
12
|
+
if match[1] == 'lib'
|
13
|
+
"spec/lib/#{match[2]}_spec.rb"
|
14
|
+
else
|
15
|
+
"spec/#{match[2]}_spec.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
if File.exist?(file_path)
|
19
|
+
file_path
|
20
|
+
else
|
21
|
+
last_run_spec
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# RSpec files
|
26
|
+
rspec = dsl.rspec
|
27
|
+
|
28
|
+
# noinspection RubyResolve
|
29
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
30
|
+
# noinspection RubyResolve
|
31
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
32
|
+
# noinspection RubyResolve
|
33
|
+
watch(rspec.spec_files) do |spec|
|
34
|
+
# noinspection RubyUnusedLocalVariable
|
35
|
+
last_run_spec = spec[0]
|
36
|
+
end
|
37
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2019 Aleck Greenham
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# JSONSpectacular
|
2
|
+
|
3
|
+
[]()
|
4
|
+
[](https://travis-ci.org/greena13/json_spectacular)
|
5
|
+
[](https://github.com/greena13/json_spectacular/blob/master/LICENSE)
|
6
|
+
|
7
|
+
JSON assertions with noise-free reports on complex nested structures.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
group :test do
|
15
|
+
gem 'json_spectacular', require: false
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
Add `json_spectacular` to your `spec/rails_helper.rb`
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
|
23
|
+
require 'json_spectacular'
|
24
|
+
|
25
|
+
# ...
|
26
|
+
|
27
|
+
RSpec.configure do |config|
|
28
|
+
# ...
|
29
|
+
|
30
|
+
%i[request controller].each do |spec_type|
|
31
|
+
config.include JSONSpectacular::RSpec, type: spec_type
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
And then execute:
|
37
|
+
|
38
|
+
```bash
|
39
|
+
bundle install
|
40
|
+
```
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
### Asserting JSON responses
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
RSpec.describe 'making some valid request', type: :request do
|
48
|
+
context 'some important context' do
|
49
|
+
it 'should return some complicated JSON' do
|
50
|
+
perform_request
|
51
|
+
|
52
|
+
expect(json_response).to eql_json([
|
53
|
+
{
|
54
|
+
"a" => [
|
55
|
+
1, 2, 3
|
56
|
+
],
|
57
|
+
"c" => { "d" => "d'"}
|
58
|
+
},
|
59
|
+
{
|
60
|
+
"b" => [
|
61
|
+
1, 2, 3
|
62
|
+
],
|
63
|
+
"c" => { "d" => "d'"}
|
64
|
+
}
|
65
|
+
])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
The `json_response` helper automatically parses the last response object as json, and the assertion `eql_json` reports failures in a format that is much clearer than anything provided by RSpec.
|
72
|
+
|
73
|
+
The full `expected` and `actual` values are still reported, but below is a separate report that only includes the paths to the failed nested values and their differences, removing the need to manually compare the two complete objects to find the difference.
|
74
|
+
|
75
|
+
## Test suite
|
76
|
+
|
77
|
+
`json_spectacular` comes with close-to-complete test coverage. You can run the test suite as follows:
|
78
|
+
|
79
|
+
```bash
|
80
|
+
rspec
|
81
|
+
```
|
82
|
+
|
83
|
+
## Contributing
|
84
|
+
|
85
|
+
1. Fork it ( https://github.com/greena13/json_spectacular/fork )
|
86
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
87
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
88
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
89
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'json_spectacular/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'json_spectacular'
|
9
|
+
spec.version = JSONSpectacular::VERSION
|
10
|
+
spec.authors = ['Aleck Greenham']
|
11
|
+
spec.email = ['greenhama13@gmail.com']
|
12
|
+
spec.summary = 'JSON assertions with noise-free reports on complex nested structures'
|
13
|
+
spec.description = 'JSON assertions that help you find exactly what has changed with ' \
|
14
|
+
'your JSON API without having to manually diff large objects'
|
15
|
+
spec.homepage = 'https://github.com/greena13/json_spectacular'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_dependency 'hashdiff', '~> 0'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
26
|
+
spec.add_development_dependency 'guard', '~> 2.1'
|
27
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.7'
|
28
|
+
spec.add_development_dependency 'rake', '~> 0'
|
29
|
+
spec.add_development_dependency 'rspec', '>= 3.5.0'
|
30
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hashdiff'
|
4
|
+
|
5
|
+
module JSONSpectacular
|
6
|
+
module DiffDescriptions
|
7
|
+
def self.included(base) # rubocop:disable Metrics/MethodLength
|
8
|
+
base.class_eval do # rubocop:disable Metrics/BlockLength
|
9
|
+
# Adds diff descriptions to the failure message until the all the nodes of the
|
10
|
+
# expected and actual values have been compared, and all the differences (and the
|
11
|
+
# paths to them) have been included.
|
12
|
+
#
|
13
|
+
# For Hashes and Arrays, it recursively calls itself to compare all nodes and
|
14
|
+
# elements.
|
15
|
+
#
|
16
|
+
# @param [String, Number, Boolean, Array, Hash] actual_value current node of the
|
17
|
+
# actual value being compared to the corresponding node of the expected
|
18
|
+
# value
|
19
|
+
# @param [String, Number, Boolean, Array, Hash] expected_value current node of
|
20
|
+
# the expected value being compared to the corresponding node of the
|
21
|
+
# actual value
|
22
|
+
# @param [String] path path to the current nodes being compared, relative to the
|
23
|
+
# root full objects
|
24
|
+
# @return void Diff descriptions are appended directly to message
|
25
|
+
def add_diff_to_message(actual_value, expected_value, path = '')
|
26
|
+
diffs_sorted_by_name = HashDiff
|
27
|
+
.diff(actual_value, expected_value)
|
28
|
+
.sort_by { |a| a[1] }
|
29
|
+
|
30
|
+
diffs_grouped_by_name =
|
31
|
+
diffs_sorted_by_name.each_with_object({}) do |diff, memo|
|
32
|
+
operator, name, value = diff
|
33
|
+
memo[name] ||= {}
|
34
|
+
memo[name][operator] = value
|
35
|
+
end
|
36
|
+
|
37
|
+
diffs_grouped_by_name.each do |name, difference|
|
38
|
+
resolve_and_append_diff_to(
|
39
|
+
path, name, expected_value, actual_value, difference
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def resolve_and_append_diff_to(
|
45
|
+
path,
|
46
|
+
name,
|
47
|
+
expected_value,
|
48
|
+
actual_value,
|
49
|
+
difference
|
50
|
+
)
|
51
|
+
extra_value, missing_value, different_value =
|
52
|
+
resolve_changes(difference, expected_value, actual_value, name)
|
53
|
+
|
54
|
+
full_path = !path.empty? ? "#{path}.#{name}" : name
|
55
|
+
|
56
|
+
if non_empty_hash?(missing_value) && non_empty_hash?(extra_value)
|
57
|
+
add_diff_to_message(missing_value, extra_value, full_path)
|
58
|
+
elsif non_empty_array?(missing_value) && non_empty_array?(extra_value)
|
59
|
+
[missing_value.length, extra_value.length].max.times do |i|
|
60
|
+
add_diff_to_message(missing_value[i], extra_value[i], full_path)
|
61
|
+
end
|
62
|
+
elsif difference.key?('~')
|
63
|
+
value = value_at_path(expected_value, name)
|
64
|
+
append_diff_to_message(full_path, value, different_value)
|
65
|
+
else
|
66
|
+
append_diff_to_message(full_path, extra_value, missing_value)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def resolve_changes(difference, expected_value, actual_value, name)
|
71
|
+
missing_value = difference['-'] || value_at_path(actual_value, name)
|
72
|
+
extra_value = difference['+'] || value_at_path(expected_value, name)
|
73
|
+
different_value = difference['~']
|
74
|
+
|
75
|
+
[extra_value, missing_value, different_value]
|
76
|
+
end
|
77
|
+
|
78
|
+
def append_diff_to_message(path, expected, actual)
|
79
|
+
append_to_message(
|
80
|
+
path,
|
81
|
+
get_diff(path, expected: expected, actual: actual)
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def non_empty_hash?(target)
|
86
|
+
target.is_a?(Hash) && target.any?
|
87
|
+
end
|
88
|
+
|
89
|
+
def non_empty_array?(target)
|
90
|
+
target.is_a?(Array) && target.any?
|
91
|
+
end
|
92
|
+
|
93
|
+
def append_to_message(attribute, diff_description)
|
94
|
+
return if already_reported_difference?(attribute)
|
95
|
+
|
96
|
+
@message += diff_description
|
97
|
+
@reported_differences[attribute] = true
|
98
|
+
end
|
99
|
+
|
100
|
+
def already_reported_difference?(attribute)
|
101
|
+
@reported_differences.key?(attribute)
|
102
|
+
end
|
103
|
+
|
104
|
+
def value_at_path(target, attribute_path)
|
105
|
+
keys = attribute_path.split(/[\[\].]/)
|
106
|
+
|
107
|
+
keys = keys.map do |key|
|
108
|
+
if key.to_i.zero? && key != '0'
|
109
|
+
key
|
110
|
+
else
|
111
|
+
key.to_i
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
result = target
|
116
|
+
|
117
|
+
keys.each do |key|
|
118
|
+
result = result[key] unless key == ''
|
119
|
+
end
|
120
|
+
|
121
|
+
result
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_diff(attribute, options = {})
|
125
|
+
diff_description = ''
|
126
|
+
diff_description += "#{attribute}\n"
|
127
|
+
diff_description += "Expected: #{format_value(options[:expected])}\n"
|
128
|
+
diff_description + "Actual: #{format_value(options[:actual])}\n\n"
|
129
|
+
end
|
130
|
+
|
131
|
+
def format_value(value)
|
132
|
+
if value.is_a?(String)
|
133
|
+
"'#{value}'"
|
134
|
+
else
|
135
|
+
value
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json_spectacular/diff_descriptions'
|
4
|
+
|
5
|
+
module JSONSpectacular
|
6
|
+
# Backing class for the eql_json RSpec matcher. Used for matching Ruby representations
|
7
|
+
# of JSON response bodies. Provides clear diff representations for simple and complex
|
8
|
+
# or nested JSON objects, highlighting only the values that are different, and where
|
9
|
+
# they are in the larger JSON object.
|
10
|
+
#
|
11
|
+
# Expected to be used as part of a RSpec test suite and with
|
12
|
+
# {JSONSpectacular::RSpec#json_response}.
|
13
|
+
#
|
14
|
+
# @see JSONSpectacular::RSpec#json_response
|
15
|
+
# @see JSONSpectacular::RSpec#eql_json
|
16
|
+
#
|
17
|
+
# @author Aleck Greenham
|
18
|
+
class Matcher
|
19
|
+
include DiffDescriptions
|
20
|
+
|
21
|
+
# Creates a new JSONSpectacular::Matcher object.
|
22
|
+
#
|
23
|
+
# @see JSONSpectacular::RSpec#eql_json
|
24
|
+
#
|
25
|
+
# @param [String, Number, Boolean, Array, Hash] expected The expected value that will
|
26
|
+
# be compared with the actual value
|
27
|
+
# @return [JSONSpectacular::Matcher] New matcher object
|
28
|
+
def initialize(expected)
|
29
|
+
@expected = expected
|
30
|
+
@message = ''
|
31
|
+
@reported_differences = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Declares that RSpec should not attempt to diff the actual and expected values
|
35
|
+
# to put in the failure message. This class takes care of diffing and presenting
|
36
|
+
# the differences, itself.
|
37
|
+
# @return [false] Always false
|
38
|
+
def diffable?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Whether the actual value and the expected value are considered equal.
|
43
|
+
# @param [String, Number, Boolean, Array, Hash] actual The value to be compared to
|
44
|
+
# <tt>expected</tt> for equality
|
45
|
+
# @return [Boolean] True when <tt>actual</tt> equals <tt>expected</tt>.
|
46
|
+
def matches?(actual)
|
47
|
+
@actual = actual
|
48
|
+
@expected.eql?(@actual)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Message to display to StdOut by RSpec if the equality check fails. Includes a
|
52
|
+
# complete serialisation of <tt>expected</tt> and <tt>actual</tt> values and is
|
53
|
+
# then followed by a description of only the (possibly deeply nested) attributes
|
54
|
+
# that are different
|
55
|
+
# @return [String] message full failure message with explanation of why actual
|
56
|
+
# failed the equality check with expected
|
57
|
+
def failure_message
|
58
|
+
@message += "Expected: #{@expected}\n\n"
|
59
|
+
@message += "Actual: #{@actual}\n\n"
|
60
|
+
@message += "Differences\n\n"
|
61
|
+
|
62
|
+
add_diff_to_message(@actual, @expected)
|
63
|
+
|
64
|
+
@message
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json_spectacular/matcher'
|
4
|
+
|
5
|
+
module JSONSpectacular
|
6
|
+
# Module containing JSON helper methods that can be mixed into RSpec test scope
|
7
|
+
#
|
8
|
+
# @author Aleck Greenham
|
9
|
+
module RSpec
|
10
|
+
# Parses the last response body in a Rails RSpec controller or request test as JSON
|
11
|
+
#
|
12
|
+
# @return [Hash{String => Boolean, String, Number, Hash, Array}] Ruby representation
|
13
|
+
# of the JSON response body.
|
14
|
+
def json_response
|
15
|
+
JSON.parse(response.body)
|
16
|
+
rescue JSON::ParserError
|
17
|
+
'< INVALID JSON RESPONSE >'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Creates a new JSONSpectacular::Matcher instance so it can be passed to RSpec to
|
21
|
+
# match <tt>expected</tt> against an actual value.
|
22
|
+
#
|
23
|
+
# @see JSONSpectacular::Matcher
|
24
|
+
#
|
25
|
+
# @example Use the eql_json expectation
|
26
|
+
# expect(actual).to eql_json(expected)
|
27
|
+
#
|
28
|
+
# @example Use the eql_json expectation with json_response
|
29
|
+
# expect(json_response).to eql_json(expected)
|
30
|
+
#
|
31
|
+
# @param [Boolean, Hash, String, Number, Array] expected The expected value the RSpec
|
32
|
+
# matcher should match against.
|
33
|
+
# @return [JSONSpectacular::Matcher] New matcher object
|
34
|
+
def eql_json(expected)
|
35
|
+
JSONSpectacular::Matcher.new(expected)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,396 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json_spectacular/rspec'
|
4
|
+
|
5
|
+
RSpec.describe 'eql_json' do
|
6
|
+
include JSONSpectacular::RSpec
|
7
|
+
|
8
|
+
context 'when comparing strings' do
|
9
|
+
let(:expected) { 'a' }
|
10
|
+
let(:actual) { 'b' }
|
11
|
+
|
12
|
+
it 'then correctly reports any differences' do
|
13
|
+
expect(actual).to eql_json(actual)
|
14
|
+
|
15
|
+
expect(actual).to_not eql(expected)
|
16
|
+
|
17
|
+
expect do
|
18
|
+
expect(actual).to eql_json(expected)
|
19
|
+
end.to raise_error.with_message(error_message(expected, actual,
|
20
|
+
'' => {
|
21
|
+
expected: "'#{expected}'",
|
22
|
+
actual: "'#{actual}'"
|
23
|
+
}))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when comparing integers' do
|
28
|
+
let(:expected) { 2 }
|
29
|
+
let(:actual) { 1 }
|
30
|
+
|
31
|
+
it 'then correctly reports any differences' do
|
32
|
+
expect(actual).to eql_json(actual)
|
33
|
+
|
34
|
+
expect(actual).to_not eql(expected)
|
35
|
+
|
36
|
+
expect do
|
37
|
+
expect(actual).to eql_json(expected)
|
38
|
+
end.to raise_error.with_message(error_message(expected, actual,
|
39
|
+
'' => {
|
40
|
+
expected: expected,
|
41
|
+
actual: actual
|
42
|
+
}))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when comparing nil' do
|
47
|
+
let(:expected) { 2 }
|
48
|
+
let(:actual) { nil }
|
49
|
+
|
50
|
+
it 'then correctly reports any differences' do
|
51
|
+
expect(actual).to eql_json(actual)
|
52
|
+
|
53
|
+
expect(actual).to_not eql(expected)
|
54
|
+
|
55
|
+
expect do
|
56
|
+
expect(actual).to eql_json(expected)
|
57
|
+
end.to raise_error.with_message(error_message(expected, actual,
|
58
|
+
'' => {
|
59
|
+
expected: expected,
|
60
|
+
actual: actual
|
61
|
+
}))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'when comparing arrays' do
|
66
|
+
let(:expected) { [1, 3, 4, 5] }
|
67
|
+
let(:actual) { [1, 2, 3] }
|
68
|
+
|
69
|
+
it 'then correctly reports the elements that have changed' do
|
70
|
+
expect(actual).to eql(actual)
|
71
|
+
|
72
|
+
expect(actual).to_not eql(expected)
|
73
|
+
|
74
|
+
begin
|
75
|
+
expect(actual).to eql_json(expected)
|
76
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
77
|
+
expect(e.message).to eql(error_message(expected, actual,
|
78
|
+
'[1]' => {
|
79
|
+
expected: 3,
|
80
|
+
actual: 2
|
81
|
+
},
|
82
|
+
'[2]' => {
|
83
|
+
expected: 4,
|
84
|
+
actual: 3
|
85
|
+
},
|
86
|
+
'[3]' => {
|
87
|
+
expected: 5,
|
88
|
+
actual: ''
|
89
|
+
}))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'when comparing arrays of objects' do
|
95
|
+
let(:expected) do
|
96
|
+
{
|
97
|
+
'alpha' => 'alpha',
|
98
|
+
'beta' => [1, 2, 3],
|
99
|
+
'gamma' => [
|
100
|
+
{ 'i' => 'a', 'j' => 'b' },
|
101
|
+
{ 'i' => 'c', 'j' => 'd' },
|
102
|
+
{ 'i' => 'e', 'j' => 'f' }
|
103
|
+
]
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
let(:actual) do
|
108
|
+
{
|
109
|
+
'alpha' => 'alpha',
|
110
|
+
'beta' => [1, 2, 3],
|
111
|
+
'gamma' => [
|
112
|
+
{ 'j' => 'b' },
|
113
|
+
{ 'i' => 'c', 'j' => 'D' },
|
114
|
+
{ 'i' => 'e', 'j' => 'f', 'k' => 'k' }
|
115
|
+
]
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'then correctly reports the elements that have changed' do
|
120
|
+
expect(actual).to eql(actual)
|
121
|
+
|
122
|
+
expect(actual).to_not eql(expected)
|
123
|
+
|
124
|
+
begin
|
125
|
+
expect(actual).to eql_json(expected)
|
126
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
127
|
+
expect(e.message).to eql(error_message(expected, actual,
|
128
|
+
'gamma[0].i' => {
|
129
|
+
expected: "'a'",
|
130
|
+
actual: ''
|
131
|
+
},
|
132
|
+
'gamma[1].j' => {
|
133
|
+
expected: "'d'",
|
134
|
+
actual: "'D'"
|
135
|
+
},
|
136
|
+
'gamma[2].k' => {
|
137
|
+
expected: '',
|
138
|
+
actual: "'k'"
|
139
|
+
}))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'when comparing objects' do
|
145
|
+
let(:expected) do
|
146
|
+
{
|
147
|
+
'a' => 'a',
|
148
|
+
'c' => 'd',
|
149
|
+
'e' => 'e'
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
let(:actual) do
|
154
|
+
{
|
155
|
+
'a' => 'a',
|
156
|
+
'b' => 'b',
|
157
|
+
'c' => 'c'
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'then correctly reports the elements that have changed' do
|
162
|
+
expect(actual).to eql(actual)
|
163
|
+
|
164
|
+
expect(actual).to_not eql(expected)
|
165
|
+
|
166
|
+
begin
|
167
|
+
expect(actual).to eql_json(expected)
|
168
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
169
|
+
expect(e.message).to eql(error_message(expected, actual,
|
170
|
+
'b' => {
|
171
|
+
expected: '',
|
172
|
+
actual: "'b'"
|
173
|
+
},
|
174
|
+
'c' => {
|
175
|
+
expected: "'d'",
|
176
|
+
actual: "'c'"
|
177
|
+
},
|
178
|
+
'e' => {
|
179
|
+
expected: "'e'",
|
180
|
+
actual: ''
|
181
|
+
}))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context 'when comparing nested objects' do
|
187
|
+
let(:actual) do
|
188
|
+
{
|
189
|
+
'a' => 'a',
|
190
|
+
'b' => {
|
191
|
+
'b' => 'b'
|
192
|
+
},
|
193
|
+
'c' => {
|
194
|
+
'd' => 'd',
|
195
|
+
'e' => 'e',
|
196
|
+
'f' => {
|
197
|
+
'g' => 'g'
|
198
|
+
},
|
199
|
+
'h' => [1, 2, 3]
|
200
|
+
},
|
201
|
+
'i' => {
|
202
|
+
'j' => 'j',
|
203
|
+
'k' => 'k'
|
204
|
+
}
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
let(:expected) do
|
209
|
+
{
|
210
|
+
'a' => 'a',
|
211
|
+
'c' => {
|
212
|
+
'e' => 'e2',
|
213
|
+
'f' => {
|
214
|
+
'g2' => 'g2'
|
215
|
+
},
|
216
|
+
'h' => [1, 2, 4]
|
217
|
+
},
|
218
|
+
'i' => {
|
219
|
+
'j' => 'j'
|
220
|
+
}
|
221
|
+
}
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'then correctly reports the elements that have changed' do
|
225
|
+
expect(actual).to eql(actual)
|
226
|
+
|
227
|
+
expect(actual).to_not eql(expected)
|
228
|
+
|
229
|
+
begin
|
230
|
+
expect(actual).to eql_json(expected)
|
231
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
232
|
+
expect(e.message).to eql(error_message(expected, actual,
|
233
|
+
'b' => {
|
234
|
+
expected: '',
|
235
|
+
actual: '{"b"=>"b"}'
|
236
|
+
},
|
237
|
+
'c.d' => {
|
238
|
+
expected: '',
|
239
|
+
actual: "'d'"
|
240
|
+
},
|
241
|
+
'c.e' => {
|
242
|
+
expected: "'e2'",
|
243
|
+
actual: "'e'"
|
244
|
+
},
|
245
|
+
'c.f.g' => {
|
246
|
+
expected: '',
|
247
|
+
actual: "'g'"
|
248
|
+
},
|
249
|
+
'c.f.g2' => {
|
250
|
+
expected: "'g2'",
|
251
|
+
actual: ''
|
252
|
+
},
|
253
|
+
'c.h[2]' => {
|
254
|
+
expected: '4',
|
255
|
+
actual: '3'
|
256
|
+
},
|
257
|
+
'i.k' => {
|
258
|
+
expected: '',
|
259
|
+
actual: "'k'"
|
260
|
+
}))
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
context 'when comparing complicated objects' do
|
266
|
+
let(:expected) do
|
267
|
+
{
|
268
|
+
'a' => 'aa',
|
269
|
+
'b' => 'bb',
|
270
|
+
'c' => {
|
271
|
+
'd' => 2,
|
272
|
+
'e' => 'ee',
|
273
|
+
'f' => [{
|
274
|
+
'g' => 'gg',
|
275
|
+
'h' => 'hh'
|
276
|
+
},
|
277
|
+
{
|
278
|
+
'g' => 'g1',
|
279
|
+
'h' => 'h1'
|
280
|
+
}],
|
281
|
+
'i' => {
|
282
|
+
'j' => 'jj',
|
283
|
+
'k' => 'kk',
|
284
|
+
'l' => [],
|
285
|
+
'm' => {
|
286
|
+
'n' => 1,
|
287
|
+
'o' => 'oo',
|
288
|
+
'p' => {
|
289
|
+
'q' => 'qq'
|
290
|
+
},
|
291
|
+
'r' => []
|
292
|
+
}
|
293
|
+
},
|
294
|
+
's' => [
|
295
|
+
{
|
296
|
+
't' => 179,
|
297
|
+
'u' => 'UU'
|
298
|
+
}
|
299
|
+
]
|
300
|
+
}
|
301
|
+
}
|
302
|
+
end
|
303
|
+
|
304
|
+
let(:actual) do
|
305
|
+
{
|
306
|
+
'a' => 'aa',
|
307
|
+
'b' => 'bb',
|
308
|
+
'c' => {
|
309
|
+
'd' => 3,
|
310
|
+
'e' => 'ee',
|
311
|
+
'f' => [{
|
312
|
+
'g' => 'g1',
|
313
|
+
'h' => 'hh'
|
314
|
+
},
|
315
|
+
{
|
316
|
+
'g' => 'g1',
|
317
|
+
'h' => 'h1',
|
318
|
+
'h2' => 'h2'
|
319
|
+
}],
|
320
|
+
'i' => {
|
321
|
+
'j' => 'j2',
|
322
|
+
'k' => 'kk',
|
323
|
+
'l' => [2],
|
324
|
+
'm' => {
|
325
|
+
'o' => 'oo',
|
326
|
+
'p' => {
|
327
|
+
'q' => 'qq'
|
328
|
+
},
|
329
|
+
'r' => []
|
330
|
+
}
|
331
|
+
},
|
332
|
+
's' => [
|
333
|
+
{
|
334
|
+
't' => 179,
|
335
|
+
'u' => 'UU'
|
336
|
+
}
|
337
|
+
]
|
338
|
+
}
|
339
|
+
}
|
340
|
+
end
|
341
|
+
|
342
|
+
it 'then correctly reports the elements that have changed' do
|
343
|
+
expect(actual).to eql(actual)
|
344
|
+
|
345
|
+
expect(actual).to_not eql(expected)
|
346
|
+
|
347
|
+
begin
|
348
|
+
expect(actual).to eql_json(expected)
|
349
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
350
|
+
expect(e.message).to eql(error_message(expected, actual,
|
351
|
+
'c.d' => {
|
352
|
+
expected: 2,
|
353
|
+
actual: 3
|
354
|
+
},
|
355
|
+
'c.f[0].g' => {
|
356
|
+
expected: "'gg'",
|
357
|
+
actual: "'g1'"
|
358
|
+
},
|
359
|
+
'c.f[1].h2' => {
|
360
|
+
expected: nil,
|
361
|
+
actual: "'h2'"
|
362
|
+
},
|
363
|
+
'c.i.j' => {
|
364
|
+
expected: "'jj'",
|
365
|
+
actual: "'j2'"
|
366
|
+
},
|
367
|
+
'c.i.l[0]' => {
|
368
|
+
expected: nil,
|
369
|
+
actual: 2
|
370
|
+
},
|
371
|
+
'c.i.m.n' => {
|
372
|
+
expected: 1,
|
373
|
+
actual: nil
|
374
|
+
}))
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
private
|
380
|
+
|
381
|
+
def error_message(expected, actual, differences)
|
382
|
+
message_lines = [
|
383
|
+
"Expected: #{expected}\n\n",
|
384
|
+
"Actual: #{actual}\n\n",
|
385
|
+
"Differences\n\n"
|
386
|
+
]
|
387
|
+
|
388
|
+
differences.each do |attribute_name, difference|
|
389
|
+
message_lines.push("#{attribute_name}\n")
|
390
|
+
message_lines.push("Expected: #{difference[:expected]}\n")
|
391
|
+
message_lines.push("Actual: #{difference[:actual]}\n\n")
|
392
|
+
end
|
393
|
+
|
394
|
+
message_lines.join
|
395
|
+
end
|
396
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: json_spectacular
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aleck Greenham
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hashdiff
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard-rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.5.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.5.0
|
97
|
+
description: JSON assertions that help you find exactly what has changed with your
|
98
|
+
JSON API without having to manually diff large objects
|
99
|
+
email:
|
100
|
+
- greenhama13@gmail.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- ".rspec"
|
107
|
+
- ".rubocop.yml"
|
108
|
+
- ".travis.yml"
|
109
|
+
- ".yardopts"
|
110
|
+
- Gemfile
|
111
|
+
- Guardfile
|
112
|
+
- LICENSE.txt
|
113
|
+
- README.md
|
114
|
+
- Rakefile
|
115
|
+
- json_spectacular.gemspec
|
116
|
+
- lib/json_spectacular.rb
|
117
|
+
- lib/json_spectacular/diff_descriptions.rb
|
118
|
+
- lib/json_spectacular/matcher.rb
|
119
|
+
- lib/json_spectacular/rspec.rb
|
120
|
+
- lib/json_spectacular/version.rb
|
121
|
+
- spec/eql_json_spec.rb
|
122
|
+
- spec/spec_helper.rb
|
123
|
+
homepage: https://github.com/greena13/json_spectacular
|
124
|
+
licenses:
|
125
|
+
- MIT
|
126
|
+
metadata: {}
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 2.5.2.1
|
144
|
+
signing_key:
|
145
|
+
specification_version: 4
|
146
|
+
summary: JSON assertions with noise-free reports on complex nested structures
|
147
|
+
test_files:
|
148
|
+
- spec/eql_json_spec.rb
|
149
|
+
- spec/spec_helper.rb
|