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 +4 -4
- data/.yardopts +1 -0
- data/lib/test_assistant/configuration.rb +109 -0
- data/lib/test_assistant/email/expectation.rb +165 -26
- data/lib/test_assistant/email/helpers.rb +20 -0
- data/lib/test_assistant/failure_reporter.rb +30 -2
- data/lib/test_assistant/json/expectation.rb +80 -35
- data/lib/test_assistant/json/helpers.rb +21 -0
- data/lib/test_assistant/version.rb +1 -1
- data/lib/test_assistant.rb +15 -1
- data/spec/eql_json_spec.rb +118 -2
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9311d43bef33250abf4b13c6f10cfa03df96f8ed
|
4
|
+
data.tar.gz: 1fb3475a4bed626f4a5744724c340ac8702888b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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?{|
|
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?{|
|
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?{|
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
94
|
+
full_path = path.length > 0 ? "#{path}.#{name}" : name
|
52
95
|
|
53
|
-
if non_empty_hash?(
|
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
|
-
|
98
|
+
add_diff_to_message(missing_value, extra_value, full_path)
|
57
99
|
|
58
|
-
|
59
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
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
|
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
|
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
|
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
|
data/lib/test_assistant.rb
CHANGED
@@ -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
|
data/spec/eql_json_spec.rb
CHANGED
@@ -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.
|
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:
|
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
|