test_assistant 0.1.1 → 0.1.2

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 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