test_assistant 0.1.1 → 0.1.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: bfe1610c486eff9f3ad4683799bcfc91c861c7f5
4
- data.tar.gz: 8bc9686c28979433d3e17fed24f983dc72fc104c
3
+ metadata.gz: 9311d43bef33250abf4b13c6f10cfa03df96f8ed
4
+ data.tar.gz: 1fb3475a4bed626f4a5744724c340ac8702888b4
5
5
  SHA512:
6
- metadata.gz: ca6bb85a6d11ebda0f0d5ac40695a7dee8fd3d2863b3c2d13f5f9c2667c05ec95d09d61efe9a08d3100f847e8ee73ef355dc0e7e0b83bc861cfb04a6dea085f0
7
- data.tar.gz: dc90720582c1a364ad13a17e4aa65e578616e71e0a6dcae9ed9d00450d9aba8bc4b1a461880872a1fbdc24c489285c5afb34d15969387f2dc503346e196db9a0
6
+ metadata.gz: '00185e2575b08c763500c145db50601143a73da0fa669c40eb70f7cc4e9156905c9b49ab13b3d4c6db8c7fe27fb82b60d9bd1d7a87df21c9472034e83c893609'
7
+ data.tar.gz: e76955c99b919c852f83de86ebb6ee173ac41acc8dbff90b8100d5784f0847fc35f09c4f03a0b3d61ee672a479aa51ba2f1f37f5cf52d8ee05e4a3a723289eb9
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ - README LICENSE
@@ -3,15 +3,56 @@ module TestAssistant
3
3
  autoload :Email, 'test_assistant/email/helpers'
4
4
  autoload :FailureReporter, 'test_assistant/failure_reporter'
5
5
 
6
+ # Class that provides configuration methods to control what parts of Test Assistant
7
+ # are included in an RSpec test suite. Instances of this class are managed internally
8
+ # by Test Assistant when using the TestAssistant#configure method.
9
+ #
10
+ # @see TestAssistant#configure
6
11
  class Configuration
12
+ # Creates a new TestAssistant::Configuration object - called internally by Test
13
+ # Assistant
14
+ #
15
+ # @param rspec_config RSpec configuration object, available in the block passed to
16
+ # RSpec.configure
17
+ # @return [TestAssistant::Configuration] new configuration object
7
18
  def initialize(rspec_config)
8
19
  @rspec_config = rspec_config
9
20
  end
10
21
 
22
+ # Configures RSpec to include the JSON helpers provided by Test Assistant in the
23
+ # the test suite's scope
24
+ #
25
+ # @see TestAssistant::Json::Helpers
26
+ # @see RSpec::Core::Configuration#include
27
+ #
28
+ # @param [Hash] options RSpec::Core::Configuration#include options
29
+ # @return void
30
+ #
31
+ # @example Include JSON helpers in your RSpec test suite
32
+ # RSpec.configure do |config|
33
+ # TestAssistant.configure(config) do |ta_config|
34
+ # ta_config.include_json_helpers
35
+ # end
36
+ # end
11
37
  def include_json_helpers(options = {})
12
38
  @rspec_config.include Json::Helpers, options
13
39
  end
14
40
 
41
+ # Configures RSpec to include the email helpers provided by Test Assistant in the
42
+ # the test suite's scope and to clear the list of emails sent after each test
43
+ #
44
+ # @see TestAssistant::Email::Helpers
45
+ # @see RSpec::Core::Configuration#include
46
+ #
47
+ # @param [Hash] options RSpec::Core::Configuration#include options
48
+ # @return void
49
+ #
50
+ # @example Include Email helpers in your RSpec test suite
51
+ # RSpec.configure do |config|
52
+ # TestAssistant.configure(config) do |ta_config|
53
+ # ta_config.include_email_helpers
54
+ # end
55
+ # end
15
56
  def include_email_helpers(options = {})
16
57
  @rspec_config.include Email::Helpers, options
17
58
 
@@ -21,9 +62,45 @@ module TestAssistant
21
62
  end
22
63
  end
23
64
 
65
+ # Configures under what circumstances a failing test should open a failure report
66
+ # detailing the last HTTP request and response in a browser
67
+ #
68
+ # @param [Hash{Symbol => Symbol,String,Boolean}] options filters for when a test
69
+ # failure should show a failure report
70
+ # @option options [Symbol, Boolean] :tag The tag tests must be given in order to
71
+ # show a failure report. If false, no tag is needed and all tests that
72
+ # fail (and meet any other filter options provided) will show a failure report.
73
+ # @option options [Symbol, Boolean] :type The RSpec test type for which a failure
74
+ # will show a failure report. If false, tests of any type that fail (and
75
+ # meet any other filter options provided) will show a failure report.
76
+ # @return void
77
+ #
78
+ # @example Show a failure report for failing tests tagged with :focus
79
+ # RSpec.configure do |config|
80
+ # TestAssistant.configure(config) do |ta_config|
81
+ # ta_config.render_failed_response(tag: :focus)
82
+ # end
83
+ # end
84
+ #
85
+ # @example Show a failure report for all failing tests
86
+ # RSpec.configure do |config|
87
+ # TestAssistant.configure(config) do |ta_config|
88
+ # ta_config.render_failed_response(tag: false)
89
+ # end
90
+ # end
91
+ #
92
+ # @example Show a failure report for all failing controller tests
93
+ # RSpec.configure do |config|
94
+ # TestAssistant.configure(config) do |ta_config|
95
+ # ta_config.render_failed_response(type: :controller)
96
+ # end
97
+ # end
98
+ #
99
+ # @see TestAssistant::FailureReporter#report
24
100
  def render_failed_responses(options = {})
25
101
  tag_filter = options[:tag]
26
102
  no_tag_filter = !tag_filter
103
+
27
104
  type_filter = options[:type]
28
105
  no_type_filter = !type_filter
29
106
 
@@ -38,6 +115,38 @@ module TestAssistant
38
115
  end
39
116
  end
40
117
 
118
+ # Configures under what circumstances a failing test should open an debugger session
119
+ #
120
+ # @param [Hash{Symbol => Symbol,String,Boolean}] options filters for when a test
121
+ # failure should open a debugger session
122
+ # @option options [Symbol, Boolean] :tag The tag tests must be given in order to
123
+ # open the debugger. If false, no tag is needed and any test that fails (and
124
+ # meets any other filter options provided) will open the debugger.
125
+ # @option options [Symbol, Boolean] :type The type of test that should open the
126
+ # debugger if it fails. If false, no tag is needed and any test that fails (and
127
+ # meets any other filter options provided) will open the debugger.
128
+ # @return void
129
+ #
130
+ # @example Open the debugger for failing tests tagged with :focus
131
+ # RSpec.configure do |config|
132
+ # TestAssistant.configure(config) do |ta_config|
133
+ # ta_config.debug_failed_responses(tag: :focus)
134
+ # end
135
+ # end
136
+ #
137
+ # @example Open the debugger for all failing tests
138
+ # RSpec.configure do |config|
139
+ # TestAssistant.configure(config) do |ta_config|
140
+ # ta_config.debug_failed_responses(tag: false)
141
+ # end
142
+ # end
143
+ #
144
+ # @example Open the debugger for all failing controller tests
145
+ # RSpec.configure do |config|
146
+ # TestAssistant.configure(config) do |ta_config|
147
+ # ta_config.debug_failed_responses(type: :controller)
148
+ # end
149
+ # end
41
150
  def debug_failed_responses(options = {})
42
151
  tag_filter = options.fetch(:tag, :debugger)
43
152
  type_filter = options[:type]
@@ -1,14 +1,22 @@
1
1
  require 'capybara/rspec'
2
2
 
3
3
  module TestAssistant::Email
4
+ # Backing class for have_been_sent declarative syntax for specifying email
5
+ # expectations. Provides ability to assert emails have been sent in a given test
6
+ # that match particular attribute values, such as sender, receiver or contents.
7
+ #
8
+ # Expected to be used as part of a RSpec test suite and with
9
+ # TestAssistant::Email::Helpers#email.
10
+ #
11
+ # Has two major components:
12
+ # - A Builder or chainable methods syntax for constructing arbitrarily specific
13
+ # expectations
14
+ # - An implementation of the same interface as RSpec custom matcher classes to
15
+ # allow evaluating those expectations those expectations
16
+ #
17
+ # @see TestAssistant::Email::Helpers#email
18
+ # @see TestAssistant::Email::Helpers#have_been_sent
4
19
  class Expectation
5
-
6
- def initialize
7
- @expectations = {}
8
- @failure_message = 'Expected email to be sent'
9
- @and_scope = nil
10
- end
11
-
12
20
  MATCHERS = {
13
21
  to: :to,
14
22
  from: :from,
@@ -18,22 +26,46 @@ module TestAssistant::Email
18
26
  actual: ->(_, email){ email.text}
19
27
  },
20
28
  matching_selector: {
21
- match: ->(_, email, value){ value.all?{|text| email.has_selector?(text) }},
29
+ match: ->(_, email, value){ value.all?{|selector| email.has_selector?(selector) }},
22
30
  actual: ->(_, email){ email.native },
23
31
  actual_name: :with_body
24
32
  },
25
33
  with_link: {
26
- match: ->(_, email, value){ value.all?{|value| email.has_selector?("a[href='#{value}']") }},
34
+ match: ->(_, email, value){ value.all?{|url| email.has_selector?("a[href='#{url}']") }},
27
35
  actual: ->(_, email){ email.native },
28
36
  actual_name: :with_body
29
37
  },
30
38
  with_image: {
31
- match: ->(_, email, value){ value.all?{|value| email.has_selector?("img[src='#{value}']") }},
39
+ match: ->(_, email, value){ value.all?{|url| email.has_selector?("img[src='#{url}']") }},
32
40
  actual: ->(_, email){ email.native },
33
41
  actual_name: :with_body
34
42
  }
35
43
  }
36
44
 
45
+ # Creates a new TestAssistant::Email::Expectation object
46
+ #
47
+ # @return [TestAssistant::Email::Expectation] new expectation object
48
+ def initialize
49
+ @expectations = {}
50
+ @failure_message = 'Expected email to be sent'
51
+ @and_scope = nil
52
+ end
53
+
54
+ #
55
+ # Expectation creation methods
56
+ #
57
+
58
+ # Allows chaining two assertions on the same email attribute together without having
59
+ # to repeat the same method. Intended as syntactical sugar only and is functionally
60
+ # equivalent to repeating the method.
61
+ #
62
+ # @example Asserting an email was sent to two email addresses
63
+ # expect(email).to have_been_sent.to('user1@email.com').and('user2@email.com')
64
+ #
65
+ # @param [Array] arguments parameters to pass to whatever assertion is being
66
+ # extended.
67
+ # @return [TestAssistant::Email::Expectation] reference to self, to allow for
68
+ # further method chaining
37
69
  def and(*arguments)
38
70
  if @and_scope
39
71
  self.send(@and_scope, *arguments)
@@ -42,9 +74,22 @@ module TestAssistant::Email
42
74
  end
43
75
  end
44
76
 
77
+ # For constructing an assertion that at least one email was sent to a particular
78
+ # email address
79
+ #
80
+ # @example Asserting an email was sent to user@email.com
81
+ # expect(email).to have_been_sent.to('user@email.com')
82
+ #
83
+ # @param [String, Array<String>] email_address address email is expected to be
84
+ # sent to. If an array of email addresses, the email is expected to have been
85
+ # sent to all of them.
86
+ # @return [TestAssistant::Email::Expectation] reference to self, to allow for
87
+ # further method chaining
45
88
  def to(email_address)
89
+ @expectations[:to] ||= []
90
+
46
91
  if email_address.kind_of?(Array)
47
- @expectations[:to] = email_address
92
+ @expectations[:to] = @expectations[:to].concat(email_address)
48
93
  else
49
94
  @expectations[:to] ||= []
50
95
  @expectations[:to] << email_address
@@ -55,6 +100,17 @@ module TestAssistant::Email
55
100
  self
56
101
  end
57
102
 
103
+ # For constructing an assertion that at least one email was sent from a particular
104
+ # email address
105
+ #
106
+ # @example Asserting an email was sent from admin@site.com
107
+ # expect(email).to have_been_sent.from('admin@site.com')
108
+ #
109
+ # @param [String] email_address address email is expected to be sent from.
110
+ # @raise ArgumentError when from is called more than once on the same expectation,
111
+ # as an email can only ben sent from a single sender.
112
+ # @return [TestAssistant::Email::Expectation] reference to self, to allow for
113
+ # further method chaining
58
114
  def from(email_address)
59
115
  if @expectations[:from]
60
116
  raise ArgumentError('An email can only have one from address, but you tried to assert the presence of 2 or more values')
@@ -67,6 +123,17 @@ module TestAssistant::Email
67
123
  self
68
124
  end
69
125
 
126
+ # For constructing an assertion that at least one email was sent with a particular
127
+ # subject line
128
+ #
129
+ # @example Asserting an email was sent with subject line 'Hello'
130
+ # expect(email).to have_been_sent.with_subject('Hello')
131
+ #
132
+ # @param [String] subject Subject line an email is expected to have been sent with
133
+ # @raise ArgumentError when with_subject is called more than once on the same
134
+ # expectation, as an email can only have one subject line.
135
+ # @return [TestAssistant::Email::Expectation] reference to self, to allow for
136
+ # further method chaining
70
137
  def with_subject(subject)
71
138
  if @expectations[:with_subject]
72
139
  raise ArgumentError('An email can only have one subject, but you tried to assert the presence of 2 or more values')
@@ -79,6 +146,15 @@ module TestAssistant::Email
79
146
  self
80
147
  end
81
148
 
149
+ # For constructing an assertion that at least one email was sent with a particular
150
+ # string in the body of the email
151
+ #
152
+ # @example Asserting an email was sent with the text 'User 1'
153
+ # expect(email).to have_been_sent.with_text('User 1')
154
+ #
155
+ # @param [String] text Text an email is expected to have been sent with in the body
156
+ # @return [TestAssistant::Email::Expectation] reference to self, to allow for
157
+ # further method chaining
82
158
  def with_text(text)
83
159
  @expectations[:with_text] ||= []
84
160
  @expectations[:with_text].push(text)
@@ -87,6 +163,16 @@ module TestAssistant::Email
87
163
  self
88
164
  end
89
165
 
166
+ # For constructing an assertion that at least one email was sent with a body that
167
+ # matches a particular CSS selector
168
+ #
169
+ # @example Asserting an email was sent with a body matching selector '#imporant-div'
170
+ # expect(email).to have_been_sent.matching_selector('#imporant-div')
171
+ #
172
+ # @param [String] selector CSS selector that should match at least one sent
173
+ # email's body
174
+ # @return [TestAssistant::Email::Expectation] reference to self, to allow for
175
+ # further method chaining
90
176
  def matching_selector(selector)
91
177
  @expectations[:matching_selector] ||= []
92
178
  @expectations[:matching_selector].push(selector)
@@ -95,6 +181,15 @@ module TestAssistant::Email
95
181
  self
96
182
  end
97
183
 
184
+ # For constructing an assertion that at least one email was sent with a link to
185
+ # a particular url in the body
186
+ #
187
+ # @example Asserting an email was sent with a link to http://www.example.com
188
+ # expect(email).to have_been_sent.with_link('http://www.example.com')
189
+ #
190
+ # @param [String] href URL that should appear in at least one sent email's body
191
+ # @return [TestAssistant::Email::Expectation] reference to self, to allow for
192
+ # further method chaining
98
193
  def with_link(href)
99
194
  @expectations[:with_link] ||= []
100
195
  @expectations[:with_link].push(href)
@@ -103,6 +198,16 @@ module TestAssistant::Email
103
198
  self
104
199
  end
105
200
 
201
+ # For constructing an assertion that at least one email was sent with an image
202
+ # hosted at a particular URL
203
+ #
204
+ # @example Asserting an email was sent with the image http://www.example.com/image.png
205
+ # expect(email).to have_been_sent.with_link('http://www.example.com/image.png')
206
+ #
207
+ # @param [String] src URL of the image that should appear in at least one sent
208
+ # email's body
209
+ # @return [TestAssistant::Email::Expectation] reference to self, to allow for
210
+ # further method chaining
106
211
  def with_image(src)
107
212
  @expectations[:with_image] ||= []
108
213
  @expectations[:with_image].push(src)
@@ -111,10 +216,21 @@ module TestAssistant::Email
111
216
  self
112
217
  end
113
218
 
219
+ #
220
+ # RSpec Matcher methods
221
+ #
222
+
223
+ # Declares that RSpec should not attempt to diff the actual and expected values
224
+ # to put in the failure message. This class takes care of diffing and presenting
225
+ # the differences, itself.
226
+ # @return [false] always returns false
114
227
  def diffable?
115
228
  false
116
229
  end
117
230
 
231
+ # Whether at least one email was sent during the current test that matches the
232
+ # constructed expectation
233
+ # @return [Boolean] whether a matching email was sent
118
234
  def matches?(emails)
119
235
  @emails = emails
120
236
 
@@ -141,6 +257,17 @@ module TestAssistant::Email
141
257
  end
142
258
  end
143
259
 
260
+ # Message to display to StdOut by RSpec if the equality check fails. Includes a
261
+ # complete a human-readable summary of the differences between what emails were
262
+ # expected to be sent, and what were actually sent (if any). Only used when the
263
+ # positive assertion is used, i.e. expect(email).to have_been_sent. For the
264
+ # failure message used for negative assertions, i.e.
265
+ # expect(email).to_not have_been_sent, see #failure_message_when_negated
266
+ #
267
+ # @see #failure_message_when_negated
268
+ #
269
+ # @return [String] message full failure message with explanation of the differences
270
+ # between what emails were expected and what was actually sent
144
271
  def failure_message
145
272
  field_descs = attribute_descriptions
146
273
  value_descs = value_descriptions
@@ -164,20 +291,14 @@ module TestAssistant::Email
164
291
  end
165
292
  end
166
293
 
167
- def result_description(field_descriptions, values)
168
- to_sentence(
169
- field_descriptions.map.with_index do |field_description, index|
170
- value = values[index]
171
-
172
- if [ 'matching selector', 'with link', 'with image' ].include?(field_description)
173
- "with body #{value}"
174
- else
175
- "#{field_description} #{value}"
176
- end
177
- end
178
- )
179
- end
180
-
294
+ # Failure message to display for negative RSpec assertions, i.e.
295
+ # expect(email).to_not have_been_sent. For the failure message displayed for positive
296
+ # assertions, see #failure_message.
297
+ #
298
+ # @see #failure_message
299
+ #
300
+ # @return [String] message full failure message with explanation of the differences
301
+ # between what emails were expected and what was actually sent
181
302
  def failure_message_when_negated
182
303
  field_descs = attribute_descriptions(negated: true)
183
304
  value_descs = value_descriptions(negated: true)
@@ -191,6 +312,20 @@ module TestAssistant::Email
191
312
 
192
313
  private
193
314
 
315
+ def result_description(field_descriptions, values)
316
+ to_sentence(
317
+ field_descriptions.map.with_index do |field_description, index|
318
+ value = values[index]
319
+
320
+ if [ 'matching selector', 'with link', 'with image' ].include?(field_description)
321
+ "with body #{value}"
322
+ else
323
+ "#{field_description} #{value}"
324
+ end
325
+ end
326
+ )
327
+ end
328
+
194
329
  def sent_email_values
195
330
  @emails.inject([]) do |memo, email|
196
331
 
@@ -295,7 +430,11 @@ module TestAssistant::Email
295
430
  end
296
431
 
297
432
  def email_body(email)
298
- email.parts.first.body.decoded
433
+ if email.parts.first
434
+ email.parts.first.body.decoded
435
+ else
436
+ email.body.encoded
437
+ end
299
438
  end
300
439
 
301
440
  def email_matches?(email, assertion, expected)
@@ -1,15 +1,35 @@
1
1
  require 'test_assistant/email/expectation'
2
2
 
3
3
  module TestAssistant::Email
4
+ # Module containing email helper methods that can be mixed into RSpec the test scope
5
+ #
6
+ # @see TestAssistant::Configuration#include_email_helpers
4
7
  module Helpers
8
+ # Syntactic sugar for referencing the list of emails sent since the start of the test
9
+ #
10
+ # @return [Array<Mail::Message>] list of sent emails
11
+ #
12
+ # @example Asserting email has been sent
13
+ # expect(email).to have_been_sent.to('test@email.com')
5
14
  def email
6
15
  ActionMailer::Base.deliveries
7
16
  end
8
17
 
18
+ # Clears the list of sent emails. Automatically called by Test Assistant at the
19
+ # end of every test.
20
+ #
21
+ # @return void
9
22
  def clear_emails
10
23
  ActionMailer::Base.deliveries = []
11
24
  end
12
25
 
26
+ # Creates a new email expectation that allows asserting emails should have specific
27
+ # attributes.
28
+ #
29
+ # @see TestAssistant::Email::Expectation
30
+ #
31
+ # @example Asserting email has been sent
32
+ # expect(email).to have_been_sent.to('test@email.com')
13
33
  def have_been_sent
14
34
  TestAssistant::Email::Expectation.new
15
35
  end
@@ -1,28 +1,56 @@
1
1
  module TestAssistant
2
+ # Factory class for generating a failure report summarising the last request and
3
+ # response sent in a particular test and opening it in a browser. Intended to
4
+ # aid in debugging and to be toggled on through the use of RSpec tags and configured
5
+ # using TestAssistant::Configuration#render_failed_responses
6
+ #
7
+ # @see TestAssistant::Configuration#render_failed_responses
2
8
  class FailureReporter
9
+ # Base class for generating, saving and opening failure reports. Those classes that
10
+ # inherit from it provide further customisations to better parse and format different
11
+ # request and response bodies, depending on their format.
3
12
  class SummaryReporter
4
13
  attr_accessor :next, :file_extension
5
14
 
15
+ # Creates a new SummaryReport object
16
+ #
17
+ # @param [ActionDispatch::Request] request the last request made before the test
18
+ # failed
19
+ # @param [ActionDispatch::TestResponse] response the response to the last request
20
+ # made before the test failed
21
+ # @param [String] extension what file extension should be used when saving the
22
+ # failure report
23
+ # @return [SummaryReport] new summary report object
6
24
  def initialize(request, response, extension = file_extension)
7
25
  @request, @response, @extension = request, response, extension
8
26
  end
9
27
 
28
+ # Writes the failure report to the tmp directory in the root of your Rails
29
+ # project so that it may be opened for viewing in an appropriate application
30
+ # depending on the failure report's file extension
31
+ #
32
+ # @return void
10
33
  def write
11
34
  File.open(file_path, 'w') do |file|
12
35
  file.write(summary)
13
36
  end
14
37
  end
15
38
 
39
+ # Opens the failure report file using an application that depends on the failure
40
+ # report's file extension. Expects that #write has already been called and the
41
+ # file exists.
42
+ #
43
+ # @return void
16
44
  def open
17
45
  system "open #{file_path}"
18
46
  end
19
47
 
48
+ protected
49
+
20
50
  def summary
21
51
  @response.body
22
52
  end
23
53
 
24
- protected
25
-
26
54
  def file_path
27
55
  @file_path ||= "#{Rails.root}/tmp/#{DateTime.now.to_i}.#{@extension}"
28
56
  end
@@ -2,22 +2,52 @@ require 'capybara/rspec'
2
2
  require 'hashdiff'
3
3
 
4
4
  module TestAssistant::Json
5
+ # Backing class for the eql_json RSpec matcher. Used for matching Ruby representations
6
+ # of JSON response bodies. Provides clear diff representations for simple and complex
7
+ # or nested JSON objects, highlighting only the values that are different, and where
8
+ # they are in the larger JSON object.
9
+ #
10
+ # Expected to be used as part of a RSpec test suite and with json_response.
11
+ #
12
+ # Implements the same interface as RSpec custom matcher classes
13
+ #
14
+ # @see TestAssistant::Json::Helpers#json_response
15
+ # @see TestAssistant::Json::Helpers#eql_json
5
16
  class Expectation
17
+ # Creates a new TestAssistant::Json::Expectation object.
18
+ #
19
+ # @see TestAssistant::Json::Helpers#eql_json
20
+ #
21
+ # @param expected the expected value that will be compared with the actual value
22
+ # @return [TestAssistant::Json::Expectation] new expectation object
6
23
  def initialize(expected)
7
24
  @expected = expected
8
25
  @message = ''
9
26
  @reported_differences = {}
10
27
  end
11
28
 
29
+ # Declares that RSpec should not attempt to diff the actual and expected values
30
+ # to put in the failure message. This class takes care of diffing and presenting
31
+ # the differences, itself.
32
+ # @return [false] always returns false
12
33
  def diffable?
13
34
  false
14
35
  end
15
36
 
37
+ # Whether the actual value and the expected value are considered equal.
38
+ # @param actual value to be compared to the expected value for equality
39
+ # @return [Boolean] whether actual is equal to expected
16
40
  def matches?(actual)
17
41
  @actual = actual
18
42
  @expected.eql?(@actual)
19
43
  end
20
44
 
45
+ # Message to display to StdOut by RSpec if the equality check fails. Includes a
46
+ # complete serialisation of the expected and actual values and is then followed
47
+ # by a description of only the (possibly deeply nested) attributes that are
48
+ # different
49
+ # @return [String] message full failure message with explanation of why actual
50
+ # failed the equality check with expected
21
51
  def failure_message
22
52
  @message += "Expected: #{@expected}\n\n"
23
53
  @message += "Actual: #{@actual}\n\n"
@@ -30,49 +60,64 @@ module TestAssistant::Json
30
60
 
31
61
  private
32
62
 
33
- def add_diff_to_message(original_actual, original_expected, parent_prefix = '')
34
- differences = HashDiff
35
- .diff(original_actual, original_expected)
63
+ # Adds diff descriptions to the failure message until the all the nodes of the
64
+ # expected and actual values have been compared and all the differences (and the
65
+ # paths to them) have been included. For Hashes and Arrays, it recursively calls
66
+ # itself to compare all nodes and elements.
67
+ #
68
+ # @param actual_value current node of the actual value being compared to the
69
+ # corresponding node of the expected value
70
+ # @param expected_value current node of the expected value being compared to
71
+ # the corresponding node of the actual value
72
+ # @param [String] path path to the current nodes being compared,
73
+ # relative to the root full objects
74
+ # @return void Diff descriptions are appended directly to message
75
+ def add_diff_to_message(actual_value, expected_value, path = '')
76
+ diffs_sorted_by_name = HashDiff
77
+ .diff(actual_value, expected_value)
36
78
  .sort{|diff1, diff2| diff1[1] <=> diff2[1]}
37
79
 
38
- grouped_differences =
39
- differences.inject({}) do |memo, diff|
40
- operator, name, value = diff
41
- memo[name] ||= {}
42
- memo[name][operator] = value
43
- memo
44
- end
80
+ diffs_grouped_by_name =
81
+ diffs_sorted_by_name.inject({}) do |memo, diff|
82
+ operator, name, value = diff
83
+ memo[name] ||= {}
84
+ memo[name][operator] = value
85
+ memo
86
+ end
87
+
88
+ diffs_grouped_by_name.each do |name, difference|
45
89
 
46
- grouped_differences.each do |name, difference|
47
- removed_value = difference['-']
48
- added_value = difference['+']
49
- swapped_value = difference['~']
90
+ missing_value = difference['-'] || value_at_path(actual_value, name)
91
+ extra_value = difference['+'] || value_at_path(expected_value, name)
92
+ different_value = difference['~']
50
93
 
51
- full_name = parent_prefix.length > 0 ? "#{parent_prefix}.#{name}" : name
94
+ full_path = path.length > 0 ? "#{path}.#{name}" : name
52
95
 
53
- if non_empty_hash?(removed_value) && non_empty_hash?(added_value)
54
- add_diff_to_message(removed_value, added_value, full_name)
96
+ if non_empty_hash?(missing_value) && non_empty_hash?(extra_value)
55
97
 
56
- elsif non_empty_array?(removed_value) && non_empty_array?(added_value)
98
+ add_diff_to_message(missing_value, extra_value, full_path)
57
99
 
58
- [removed_value.length, added_value.length].max.times do |i|
59
- add_diff_to_message(removed_value[i], added_value[i], full_name)
100
+ elsif non_empty_array?(missing_value) && non_empty_array?(extra_value)
101
+
102
+ [ missing_value.length, extra_value.length ].max.times do |i|
103
+ add_diff_to_message(missing_value[i], extra_value[i], full_path)
60
104
  end
105
+
61
106
  else
62
107
  if difference.has_key?('~')
63
- add_diff_description(full_name,
64
- format_diff(
65
- full_name,
66
- attribute_value(original_expected, name),
67
- swapped_value
108
+ append_to_message(full_path,
109
+ get_diff(
110
+ full_path,
111
+ expected: value_at_path(expected_value, name),
112
+ actual: different_value
68
113
  )
69
114
  )
70
115
  else
71
- add_diff_description(full_name,
72
- format_diff(
73
- full_name,
74
- added_value || attribute_value(original_expected, name),
75
- removed_value || attribute_value(original_actual, name)
116
+ append_to_message(full_path,
117
+ get_diff(
118
+ full_path,
119
+ expected: extra_value,
120
+ actual: missing_value
76
121
  )
77
122
  )
78
123
  end
@@ -89,7 +134,7 @@ module TestAssistant::Json
89
134
  target.kind_of?(Array) && target.any?
90
135
  end
91
136
 
92
- def add_diff_description(attribute, difference_description)
137
+ def append_to_message(attribute, difference_description)
93
138
  unless already_reported_difference?(attribute)
94
139
  @message += difference_description
95
140
  @reported_differences[attribute] = true
@@ -100,7 +145,7 @@ module TestAssistant::Json
100
145
  !!@reported_differences[attribute]
101
146
  end
102
147
 
103
- def attribute_value(target, attribute_path)
148
+ def value_at_path(target, attribute_path)
104
149
  keys = attribute_path.split(/\[|\]|\./)
105
150
 
106
151
  keys = keys.map do |key|
@@ -125,11 +170,11 @@ module TestAssistant::Json
125
170
  result
126
171
  end
127
172
 
128
- def format_diff(attribute, expected, actual)
173
+ def get_diff(attribute, options = {})
129
174
  diff_description = ''
130
175
  diff_description += "#{attribute}\n"
131
- diff_description += "Expected: #{format_value(expected)}\n"
132
- diff_description += "Actual: #{format_value(actual)}\n\n"
176
+ diff_description += "Expected: #{format_value(options[:expected])}\n"
177
+ diff_description + "Actual: #{format_value(options[:actual])}\n\n"
133
178
  end
134
179
 
135
180
  def format_value(value)
@@ -1,7 +1,15 @@
1
1
  require 'test_assistant/json/expectation'
2
2
 
3
3
  module TestAssistant::Json
4
+ # Module containing JSON helper methods that can be mixed into RSpec the test scope
5
+ #
6
+ # @see TestAssistant::Configuration#include_json_helpers
4
7
  module Helpers
8
+ # Parses the last response body in a Rails RSpec controller or request test as JSON
9
+ #
10
+ # @return [Hash{String => String, Number, Hash, Array}] Ruby representation of
11
+ # the JSON response body
12
+ # @raise []JSON::ParserError] when the response body is not valid JSON
5
13
  def json_response
6
14
  begin
7
15
  JSON.parse(response.body)
@@ -10,6 +18,19 @@ module TestAssistant::Json
10
18
  end
11
19
  end
12
20
 
21
+ # Creates a new TestAssistant::Json::Expectation instance so it can be passed
22
+ # to RSpec to match against an actual value.
23
+ #
24
+ # @see TestAssistant::Expectation
25
+ #
26
+ # @param expected the expected value the RSpec matcher should match against
27
+ # @return [TestAssistant::Json::Expectation] new expectation object
28
+ #
29
+ # @example Use the eql_json expectation
30
+ # expect(actual).to eql_json(expected)
31
+ #
32
+ # @example Use the eql_json expectation with json_response
33
+ # expect(json_response).to eql_json(expected)
13
34
  def eql_json(expected)
14
35
  TestAssistant::Json::Expectation.new(expected)
15
36
  end
@@ -1,3 +1,3 @@
1
1
  module TestAssistant
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -1,8 +1,23 @@
1
1
  require "test_assistant/version"
2
2
  require 'test_assistant/configuration'
3
3
 
4
+ # Utility module for working with RSpec test suites for Rails or similar applications
5
+ #
6
+ # Contains:
7
+ # - Expressive syntax for asserting testing emails
8
+ # - Sophisticated JSON matchers and diff failure reports
9
+ # - Rendering and debugging tools for viewing test failures
10
+ #
11
+ # @see https://github.com/greena13/test_assistant Test Assistant Github page
4
12
  module TestAssistant
5
13
  class << self
14
+ # Configures what parts of TestAssistant are included in your test suite and how
15
+ # they behave.
16
+ #
17
+ # @see TestAssistant::Configuration
18
+ #
19
+ # @param rspec_config RSpec configuration object available in RSpec.configure block
20
+ # @return void
6
21
  def configure(rspec_config)
7
22
  configuration = Configuration.new(rspec_config)
8
23
 
@@ -13,5 +28,4 @@ module TestAssistant
13
28
 
14
29
  alias :config :configure
15
30
  end
16
-
17
31
  end
@@ -289,9 +289,126 @@ RSpec.describe "eql_json" do
289
289
  end
290
290
 
291
291
  end
292
-
293
292
  end
294
293
 
294
+ context "when comparing complicated objects" do
295
+
296
+ let(:expected ) { {
297
+ "a" => "aa",
298
+ "b" => "bb",
299
+ "c" => {
300
+ "d" => 2,
301
+ "e" => "ee",
302
+ "f" => [{
303
+ "g" => "gg",
304
+ "h" => "hh",
305
+ },
306
+ {
307
+ "g" => "g1",
308
+ "h" => "h1",
309
+ }
310
+ ],
311
+ "i" => {
312
+ "j" => "jj",
313
+ "k" => "kk",
314
+ "l" => [],
315
+ "m" => {
316
+ "n" => 1,
317
+ "o" => "oo",
318
+ "p" => {
319
+ "q" => "qq"
320
+ },
321
+ "r" => [],
322
+ },
323
+ },
324
+ "s" => [
325
+ {
326
+ "t" => 179,
327
+ "u" => "UU"
328
+ }
329
+ ]
330
+ }
331
+ } }
332
+
333
+ let(:actual) { {
334
+ "a" => "aa",
335
+ "b" => "bb",
336
+ "c" => {
337
+ "d" => 3,
338
+ "e" => "ee",
339
+ "f" => [{
340
+ "g" => "g1",
341
+ "h" => "hh",
342
+ },
343
+ {
344
+ "g" => "g1",
345
+ "h" => "h1",
346
+ "h2" => "h2"
347
+ }
348
+ ],
349
+ "i" => {
350
+ "j" => "j2",
351
+ "k" => "kk",
352
+ "l" => [2],
353
+ "m" => {
354
+ "o" => "oo",
355
+ "p" => {
356
+ "q" => "qq"
357
+ },
358
+ "r" => [],
359
+ },
360
+ },
361
+ "s" => [
362
+ {
363
+ "t" => 179,
364
+ "u" => "UU"
365
+ }
366
+ ]
367
+ }
368
+ } }
369
+
370
+ it "then correctly reports the elements that have changed" do
371
+
372
+ expect(actual).to eql(actual)
373
+
374
+ expect(actual).to_not eql(expected)
375
+
376
+ begin
377
+ expect(actual).to eql_json(expected)
378
+ rescue RSpec::Expectations::ExpectationNotMetError => e
379
+
380
+ expect(e.message).to eql(error_message(expected, actual, {
381
+ 'c.d' => {
382
+ expected: 2,
383
+ actual: 3
384
+ },
385
+ 'c.f[0].g' => {
386
+ expected: "'gg'",
387
+ actual: "'g1'"
388
+ },
389
+ 'c.f[1].h2' => {
390
+ expected: nil,
391
+ actual: "'h2'"
392
+ },
393
+ 'c.i.j' => {
394
+ expected: "'jj'",
395
+ actual: "'j2'"
396
+ },
397
+ 'c.i.l[0]' => {
398
+ expected: nil,
399
+ actual: 2
400
+ },
401
+ 'c.i.m.n' => {
402
+ expected: 1,
403
+ actual: nil
404
+ }
405
+ }))
406
+
407
+ end
408
+
409
+ end
410
+
411
+ end
295
412
 
296
413
  private
297
414
 
@@ -308,7 +425,6 @@ RSpec.describe "eql_json" do
308
425
  message_lines.push("Actual: #{difference[:actual]}\n\n")
309
426
  end
310
427
 
311
-
312
428
  message_lines.join
313
429
  end
314
430
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test_assistant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleck Greenham
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-30 00:00:00.000000000 Z
11
+ date: 2018-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -124,6 +124,7 @@ extra_rdoc_files: []
124
124
  files:
125
125
  - ".gitignore"
126
126
  - ".rspec"
127
+ - ".yardopts"
127
128
  - Gemfile
128
129
  - Guardfile
129
130
  - LICENSE.txt
@@ -162,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
163
  version: '0'
163
164
  requirements: []
164
165
  rubyforge_project:
165
- rubygems_version: 2.5.1
166
+ rubygems_version: 2.5.2.1
166
167
  signing_key:
167
168
  specification_version: 4
168
169
  summary: A toolbox for increased testing efficiency with RSpec