test_assistant 0.0.4 → 0.1.0

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: 49e305f2819ade85d5d24f684b4793021fc5b9a4
4
- data.tar.gz: d665d5ac5bb5ac475e76c897fbb755cef7bb2a3d
3
+ metadata.gz: 9b1c5794fb9961343ed9b9c926125a1e7f4128bb
4
+ data.tar.gz: 8dfad5f30319bffaac95db75a76c99228bbe3fad
5
5
  SHA512:
6
- metadata.gz: 398f6cfb2cde42b5d13b1f1daf392986400258bfa1e64e7e3c73b0215078fd9adfe458c3085d2e345d796ef15e3aa798e2162dd6ed082b2261ae148b3f7cc1f1
7
- data.tar.gz: b571f52714417646144182f5051a88346d8b46b52538f68ecd478cc850e4494dc4453b5c7d4b6c5b4e50c4d499f20a627e7e608d8a128aed3e570a836b527d56
6
+ metadata.gz: 3e8db6c937c7895f188fc16c03f2fbf2344cfbd0e8621ccd42521cc380819ac259a05eac1e869e5036e9e1ec2f4ee1e57bf23ad13098ef32496abacd0adea298
7
+ data.tar.gz: f841a64f96693ebc1fd9422c931c8f626531f74e38935049bfc2851a3d0de7cae8250c84f5b84be276943f883a6eb12efa102297ef16b0f3ac19dcd751072c5e
data/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # TestAssistant
2
2
 
3
- A collection of testing tools and utilities for writing and fixing tests faster
3
+ RSpec toolbox for writing and diagnosing Ruby on Rails tests, faster - especially emails and JSON APIs.
4
4
 
5
- ## Stability
6
-
7
- TestAssistant is in its infancy and should be considered unstable. The API and behaviour is likely to change.
5
+ ## Features
8
6
 
7
+ * Light-weight, scoped, lazily executed and composable tool-box, so you only include the features you want to use, when you want to use them with no unnecessary overhead
8
+ * JSON assertion that gives noise-free reports on complex nested structures, so you can find out exactly what has changed with your JSON API without having to manually diff large objects
9
+ * Expressive email assertions that let you succinctly describe when emails should and should not sent
10
+ * Automatic reporting of the context around failing tests, so you don't have to re-run them with additional logging or a debugger
9
11
 
10
12
  ## Installation
11
13
 
@@ -19,10 +21,6 @@ And then execute:
19
21
 
20
22
  $ bundle
21
23
 
22
- Or install it yourself as:
23
-
24
- $ gem install test_assistant
25
-
26
24
  ## Usage
27
25
 
28
26
  Test assistant requires access to the RSpec configuration object, so add the following to either `rails_helper.rb` or `spec_helper.rb`:
@@ -30,131 +28,217 @@ Test assistant requires access to the RSpec configuration object, so add the fol
30
28
  ```ruby
31
29
  RSpec.configure do |config|
32
30
  # other rspec configuration
33
-
31
+
34
32
  TestAssistant.configure(config) do |ta_config|
35
33
  # test assistant configuration here
36
34
  end
37
35
  end
38
36
  ```
39
37
 
40
- ### Rendering a response context when a test fails
38
+ ## JSON expectations
39
+
40
+ Test Assistant lets you include helpers in your controller and request specs to get succinct declarative methods for defining the expected results for JSON responses.
41
41
 
42
- Test assistant can automatically render the server response in your browser when a test fails and you have applied a nominated tag.
42
+ ### Setup
43
43
 
44
44
  ```ruby
45
45
  TestAssistant.configure(config) do |ta_config|
46
- ta_config.render_failed_responses tag: :focus, type: :request
46
+ ta_config.include_json_helpers type: :request
47
47
  end
48
48
  ```
49
49
 
50
+ ### Asserting JSON responses
51
+
52
+ Among the helpers provided are `json_response`, which automatically parses the last response object as json, and a custom assertion `eql_json` that reports failures in a format that is much clearer than anything provided by RSpec.
53
+
54
+ The full `expected` and `actual` values are still reported, but below is a separate report that only includes the paths to the failed nested values and their differences, removing the need to manually compare the two complete objects to find the difference.
55
+
50
56
  ```ruby
51
57
  RSpec.describe 'making some valid request', type: :request do
52
58
  context 'some important context' do
53
- it 'should return a correct result', focus: true do
54
- # failing assertions
59
+ it 'should return some complicated JSON' do
60
+
61
+ perform_request
62
+
63
+ expect(json_response).to eql_json([
64
+ {
65
+ "a" => [
66
+ 1, 2, 3
67
+ ],
68
+ "c" => { "d" => "d'"}
69
+ },
70
+ {
71
+ "b" => [
72
+ 1, 2, 3
73
+ ],
74
+ "c" => { "d" => "d'"}
75
+ }
76
+ ])
55
77
  end
56
78
  end
57
79
  end
58
80
  ```
59
81
 
60
- ### Invoking a debugger when a test fails
61
-
62
- It's possible to invoke a debugger (`pry` is default, but fallback is to `byebug` and then `debugger`) if a test fails. This gives you access to some of the scope that the failing test ran in, allowing you to inspect objects and test variations of the failing assertion.
82
+ ## Email expectations
63
83
 
64
- The `debug_failed_responses` accepts a the following options:
84
+ Test Assistant provides a declarative API for describing when emails should be sent and their characteristics.
65
85
 
66
- * `tag: :<tag_name>` (default is `:debugger`)
67
- * `type: :<spec_type>` (default: nil - matches all test types) options.
86
+ ### Setup
68
87
 
69
88
  ```ruby
70
89
  TestAssistant.configure(config) do |ta_config|
71
- ta_config.include_json_helpers type: :request
90
+ ta_config.include_email_helpers type: :request
91
+ end
92
+ ```
72
93
 
73
- ta_config.debug_failed_responses tag: :debugger
74
- end
94
+ ### Clearing emails
95
+
96
+ Emails can be cleared at any point by calling `clear_emails` in your tests. This is helpful when you are testing a user workflow that may trigger multiple emails.
97
+
98
+ Emails are automatically cleared between each request spec.
99
+
100
+ ### Email receiver address
101
+
102
+ It's possible to assert an email was sent to one or more or more addresses using the following format:
103
+
104
+ ```ruby
105
+ expect(email).to have_been_sent.to('user@email.com')
75
106
  ```
76
107
 
108
+ ### Email sender address
109
+
110
+ Similarly, you can assert an email was sent from an address:
111
+
77
112
  ```ruby
78
- RSpec.describe 'making some valid request', type: :request do
79
- context 'some important context' do
80
- it 'should return a correct result', debugger: true do
81
- # failing assertions
82
- end
83
- end
84
- end
113
+ expect(email).to have_been_sent.from('user@email.com')
85
114
  ```
86
115
 
87
- ## JSON expectations
116
+ ### Email subject
117
+
118
+ You can assert an email's subject:
88
119
 
89
- RSpec's failed equality reports are often extremely difficult to interpret when dealing with complicated JSON objects. A minor difference, deeply nested in arrays and objects can take a long time to locate because RSpec dumps the entire object in the failure message.
90
-
120
+ ```ruby
121
+ expect(email).to have_been_sent.with_subject('Welcome!')
122
+ ```
91
123
 
92
- When enabled, Test Assistant provides a method `json_response` that automatically parses the last response object as json and a custom assertion `eql_json` that reports failed equality of complicated json objects in a format that is much clearer. The full `expected` and `actual` values are still reported, but below them Test Assistant prints only the paths to the failed nested values and their differences, removing the need to manually compare the two complete objects to find what is different.
124
+
125
+ ### Email body
126
+
127
+ You can assert the body of an email by text:
128
+
129
+ ```ruby
130
+ expect(email).to have_been_sent.with_text('Welcome, user@email.com')
131
+ ```
132
+
133
+ Or using a selector on the email's HTML:
134
+
135
+ ```ruby
136
+ expect(email).to have_been_sent.with_selector('#password')
137
+ ```
138
+
139
+ Or look for links:
140
+
141
+ ```ruby
142
+ expect(email).to have_been_sent.with_link('www.site.com/onboarding/1')
143
+ ```
144
+
145
+ Or images:
146
+
147
+ ```ruby
148
+ expect(email).to have_been_sent.with_image('www.site.com/assets/images/welcome.png')
149
+ ```
150
+
151
+ ### Chaining assertions
152
+
153
+ You can chain any combination of the above that you want for ultra specific assertions:
154
+
155
+
156
+ ```ruby
157
+ expect(email).to have_been_sent
158
+ .to('user@email.com')
159
+ .from('admin@site.com')
160
+ .with_subject('Welcome!')
161
+ .with_text('Welcome, user@email.com')
162
+ .with_selector('#password').and('#username')
163
+ .with_link('www.site.com/onboarding/1')
164
+ .with_image('www.site.com/assets/images/welcome.png')
165
+
166
+ ```
167
+
168
+ You can also chain multiple assertions of the the same type with the `and` method:
169
+
170
+ ```ruby
171
+ expect(email).to have_been_sent
172
+ .with_text('Welcome, user@email.com').and('Thanks for signing up')
173
+ ```
174
+
175
+ ### Asserting emails are NOT sent
176
+
177
+ The `have_sent_email` assertion works with the negative case as well:
178
+
179
+ ```ruby
180
+ expect(email).to_not have_been_sent.with_text('Secret token')
181
+ ```
182
+
183
+ ## Failure Reporting
184
+
185
+
186
+
187
+ ### Rendering a response context when a test fails
188
+
189
+ Test Assistant can automatically render the server response in your browser when a test fails and you have applied a nominated tag.
93
190
 
94
191
  ```ruby
95
192
  TestAssistant.configure(config) do |ta_config|
96
- ta_config.include_json_helpers type: :request
193
+ ta_config.render_failed_responses tag: :focus, type: :request
97
194
  end
98
195
  ```
99
196
 
100
197
  ```ruby
101
198
  RSpec.describe 'making some valid request', type: :request do
102
199
  context 'some important context' do
103
- it 'should return some complicated JSON' do
104
-
105
- perform_request
106
-
107
- expect(json_response).to eql_json([
108
- {
109
- "a" => [
110
- 1, 2, 3
111
- ],
112
- "c" => { "d" => "d'"}
113
- },
114
- {
115
- "b" => [
116
- 1, 2, 3
117
- ],
118
- "c" => { "d" => "d'"}
119
- }
120
- ])
200
+ it 'should return a correct result', focus: true do
201
+ # failing assertions
121
202
  end
122
203
  end
123
204
  end
124
205
  ```
125
206
 
126
- ## Email expectations
207
+ ### Invoking a debugger when a test fails
127
208
 
209
+ It's possible to invoke a debugger (`pry` is default, but fallback is to `byebug` and then `debugger`) if a test fails. This gives you access to some of the scope that the failing test ran in, allowing you to inspect objects and test variations of the failing assertion.
210
+
211
+ The `debug_failed_responses` accepts a the following options:
212
+
213
+ * `tag: :<tag_name>` (default is `:debugger`)
214
+ * `type: :<spec_type>` (default: nil - matches all test types) options.
128
215
 
129
216
  ```ruby
130
217
  TestAssistant.configure(config) do |ta_config|
131
- ta_config.include_email_helpers type: :request
132
- end
218
+ ta_config.include_json_helpers type: :request
219
+
220
+ ta_config.debug_failed_responses tag: :debugger
221
+ end
133
222
  ```
134
223
 
135
224
  ```ruby
136
225
  RSpec.describe 'making some valid request', type: :request do
137
226
  context 'some important context' do
138
- it 'should send an email' do
139
- expect(email).to have_been_sent
140
- .to('user@email.com')
141
- .from('admin@site.com')
142
- .with_subject('Welcome!')
143
- .with_text('Welcome, user@email.com').and('Thanks for signing up')
144
- .with_selector('#password').and('#username')
145
- .with_link('www.site.com/onboarding/1')
146
- .with_image('www.site.com/assets/images/welcome.png')
147
-
148
- clear_emails
149
-
150
- # further actions
151
-
152
- expect(email).to have_been_sent.to('user@email.com')
227
+ it 'should return a correct result', debugger: true do
228
+ # failing assertions
153
229
  end
154
230
  end
155
231
  end
156
232
  ```
157
233
 
234
+ ## Test suite
235
+
236
+ TestAssistant comes with close to complete test coverage. You can run the test suite as follows:
237
+
238
+ ```bash
239
+ rspec
240
+ ```
241
+
158
242
  ## Contributing
159
243
 
160
244
  1. Fork it ( https://github.com/greena13/test_assistant/fork )
@@ -14,6 +14,11 @@ module TestAssistant
14
14
 
15
15
  def include_email_helpers(options = {})
16
16
  @rspec_config.include Email::Helpers, options
17
+
18
+ @rspec_config.after :each, type: :request do
19
+ # clear emails after every request spec
20
+ ActionMailer::Base.deliveries = []
21
+ end
17
22
  end
18
23
 
19
24
  def render_failed_responses(options = {})
@@ -2,6 +2,7 @@ require 'capybara/rspec'
2
2
 
3
3
  module TestAssistant::Email
4
4
  class Expectation
5
+
5
6
  def initialize
6
7
  @expectations = {}
7
8
  @failure_message = 'Expected email to be sent'
@@ -16,7 +17,7 @@ module TestAssistant::Email
16
17
  match: ->(_, email, value){ value.all?{|text| email.has_content?(text) }},
17
18
  actual: ->(_, email){ email.text}
18
19
  },
19
- with_selector: {
20
+ matching_selector: {
20
21
  match: ->(_, email, value){ value.all?{|text| email.has_selector?(text) }},
21
22
  actual: ->(_, email){ email.native },
22
23
  actual_name: :with_body
@@ -55,11 +56,10 @@ module TestAssistant::Email
55
56
  end
56
57
 
57
58
  def from(email_address)
58
- if email_address.kind_of?(Array)
59
- @expectations[:from] = email_address
59
+ if @expectations[:from]
60
+ raise ArgumentError('An email can only have one from address, but you tried to assert the presence of 2 or more values')
60
61
  else
61
- @expectations[:from] ||= []
62
- @expectations[:from] << email_address
62
+ @expectations[:from] = email_address
63
63
  end
64
64
 
65
65
  @and_scope = :from
@@ -74,6 +74,8 @@ module TestAssistant::Email
74
74
  @expectations[:with_subject] = subject
75
75
  end
76
76
 
77
+ @and_scope = :with_subject
78
+
77
79
  self
78
80
  end
79
81
 
@@ -85,11 +87,11 @@ module TestAssistant::Email
85
87
  self
86
88
  end
87
89
 
88
- def with_selector(selector)
89
- @expectations[:with_selector] ||= []
90
- @expectations[:with_selector].push(selector)
90
+ def matching_selector(selector)
91
+ @expectations[:matching_selector] ||= []
92
+ @expectations[:matching_selector].push(selector)
91
93
 
92
- @and_scope = :with_selector
94
+ @and_scope = :matching_selector
93
95
  self
94
96
  end
95
97
 
@@ -118,70 +120,169 @@ module TestAssistant::Email
118
120
 
119
121
  matching_emails = @emails
120
122
 
121
- @expectations.each do |attribute, expected|
122
-
123
- matching_emails =
124
- matching_emails.select do |email|
125
- email_matches?(email, MATCHERS[attribute], expected)
126
- end
127
-
128
- if matching_emails.empty?
123
+ if @expectations.any?
124
+ @expectations.each do |attribute, expected|
129
125
  @failed_attribute = attribute
130
126
  @failed_expected = expected
131
- return false
127
+
128
+ matching_emails =
129
+ matching_emails.select do |email|
130
+ email_matches?(email, MATCHERS[attribute], expected)
131
+ end
132
+
133
+ if matching_emails.empty?
134
+ return false
135
+ end
132
136
  end
133
- end
134
137
 
135
- true
138
+ true
139
+ else
140
+ @emails.any?
141
+ end
136
142
  end
137
143
 
138
144
  def failure_message
139
- field_description = @failed_attribute.to_s.gsub('_', ' ')
140
- value_description =
141
- case @failed_expected
142
- when String
143
- "'#{@failed_expected}'"
144
- when Array
145
- @failed_expected.map{|val| "'#{val}'"}.to_sentence
146
- else
147
- @failed_expected
148
- end
145
+ field_descs = attribute_descriptions
146
+ value_descs = value_descriptions
149
147
 
150
- base_clause = "Expected an email to be sent #{field_description} #{value_description}."
148
+ base_clause = expectation_description(
149
+ 'Expected an email to be sent',
150
+ field_descs,
151
+ value_descs
152
+ )
151
153
 
152
154
  if @emails.length == 0
153
- base_clause + ' However, no emails were sent.'
155
+ "#{base_clause} However, no emails were sent."
154
156
  else
155
- pluralisation = @emails.length == 1 ? 'email' : 'emails'
157
+ email_values = sent_email_values
158
+
159
+ if email_values.any?
160
+ base_clause + " However, #{email_pluralisation(@emails)} sent #{result_description(field_descs, [to_sentence(email_values)])}."
161
+ else
162
+ base_clause
163
+ end
164
+ end
165
+ end
166
+
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
+
181
+ def failure_message_when_negated
182
+ field_descs = attribute_descriptions(negated: true)
183
+ value_descs = value_descriptions(negated: true)
184
+
185
+ expectation_description(
186
+ 'Expected no emails to be sent',
187
+ field_descs,
188
+ value_descs
189
+ )
190
+ end
191
+
192
+ private
193
+
194
+ def sent_email_values
195
+ @emails.inject([]) do |memo, email|
156
196
 
157
- email_values = @emails.inject([]) do |memo, email|
197
+ if [ :matching_selector, :with_link, :with_image ].include?(@failed_attribute)
198
+ memo << email_body(email)
199
+ else
158
200
  matcher = MATCHERS[@failed_attribute]
159
201
 
160
202
  value =
161
203
  case matcher
162
- when String, Symbol
163
- email.send(matcher)
164
- when Hash
165
- field_description = matcher[:actual_name] if matcher[:actual_name]
166
- matcher[:actual].(email, parsed_emails(email))
204
+ when String, Symbol
205
+ email.send(matcher)
206
+ when Hash
207
+ field_description = matcher[:actual_name] if matcher[:actual_name]
208
+ matcher[:actual].(email, parsed_emails(email))
167
209
  end
168
210
 
169
211
  value = value.kind_of?(String) ? "'#{value}'" : value
170
212
  memo << value
213
+ end
214
+
215
+ memo
216
+ end
217
+ end
218
+
219
+ def expectation_description(base_clause, field_descriptions, value_descriptions)
220
+ description = base_clause
171
221
 
172
- memo
222
+ additional_clauses = []
223
+
224
+ field_descriptions.each.with_index do |field_description, index|
225
+ clause = ''
226
+ clause += " #{field_description}" if field_description.length > 0
227
+
228
+ if (value_description = value_descriptions[index])
229
+ clause += " #{value_description}"
173
230
  end
174
231
 
232
+ additional_clauses.push(clause) if clause.length > 0
233
+ end
175
234
 
176
- if email_values.any?
177
- base_clause + " However, #{pluralisation} were sent #{field_description} #{email_values.to_sentence}"
235
+ description + additional_clauses.join('') + '.'
236
+ end
237
+
238
+ def attribute_descriptions(negated: false)
239
+ attributes_to_describe =
240
+ if negated
241
+ @expectations.keys
242
+ else
243
+ [ @failed_attribute ]
244
+ end
245
+
246
+ attributes_to_describe.map do |attribute|
247
+ attribute.to_s.gsub('_', ' ')
248
+ end
249
+ end
250
+
251
+ def value_descriptions(negated: false)
252
+ values_to_describe =
253
+ if negated
254
+ @expectations.values
255
+ else
256
+ [ @failed_expected ]
257
+ end
258
+
259
+ values_to_describe.map do |value|
260
+ case value
261
+ when String
262
+ "'#{value}'"
263
+ when Array
264
+ to_sentence(value.map{|val| "'#{val}'"})
178
265
  else
179
- base_clause
266
+ value
180
267
  end
181
268
  end
269
+
182
270
  end
183
271
 
184
- private
272
+ def email_pluralisation(emails)
273
+ emails.length > 2 ? "#{emails.length} were": "1 was"
274
+ end
275
+
276
+ def to_sentence(items)
277
+ case items.length
278
+ when 0, 1
279
+ items.join('')
280
+ when 2
281
+ items.join(' and ')
282
+ else
283
+ items[0..(items.length-3)].join(', ') + items[(items.length-3)..items.length-1].join(' and ')
284
+ end
285
+ end
185
286
 
186
287
  def parsed_emails(email)
187
288
  @parsed_emails ||= {}
@@ -190,13 +291,19 @@ module TestAssistant::Email
190
291
  end
191
292
 
192
293
  def parser(email)
193
- Capybara::Node::Simple.new(email.parts.first.body.decoded)
294
+ Capybara::Node::Simple.new(email_body(email))
295
+ end
296
+
297
+ def email_body(email)
298
+ email.parts.first.body.decoded
194
299
  end
195
300
 
196
301
  def email_matches?(email, assertion, expected)
197
302
  case assertion
303
+ when :to
304
+ expected.include?(email.send(assertion))
198
305
  when String, Symbol
199
- email.send(assertion) == expected
306
+ expected == email.send(assertion)
200
307
  when Hash
201
308
  assertion[:match].(email, parsed_emails(email), expected)
202
309
  else
@@ -5,6 +5,8 @@ module TestAssistant::Json
5
5
  class Expectation
6
6
  def initialize(expected)
7
7
  @expected = expected
8
+ @message = ''
9
+ @reported_differences = {}
8
10
  end
9
11
 
10
12
  def diffable?
@@ -12,9 +14,6 @@ module TestAssistant::Json
12
14
  end
13
15
 
14
16
  def matches?(actual)
15
- @message = ''
16
- @reported_differences = {}
17
-
18
17
  @actual = actual
19
18
  @expected.eql?(@actual)
20
19
  end
@@ -24,36 +23,71 @@ module TestAssistant::Json
24
23
  @message += "Actual: #{@actual}\n\n"
25
24
  @message += "Differences\n\n"
26
25
 
27
- differences = HashDiff.diff(@actual, @expected)
26
+ add_diff_to_message(@actual, @expected)
28
27
 
29
- differences.each do |difference|
30
- operator, *operands = difference
28
+ @message
29
+ end
31
30
 
32
- case operator
33
- when '-'
34
- attribute, value = operands
31
+ private
35
32
 
36
- expected_value = attribute_value(@expected, attribute)
37
- add_diff_description(attribute, format_diff(attribute, expected_value, value))
33
+ def add_diff_to_message(original_actual, original_expected, parent_prefix = '')
34
+ differences = HashDiff
35
+ .diff(original_actual, original_expected)
36
+ .sort{|diff1, diff2| diff1[1] <=> diff2[1]}
38
37
 
39
- when '+'
40
- attribute, value = operands
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
41
45
 
42
- actual_value = attribute_value(@actual, attribute)
46
+ grouped_differences.each do |name, difference|
47
+ removed_value = difference['-']
48
+ added_value = difference['+']
49
+ swapped_value = difference['~']
43
50
 
44
- add_diff_description(attribute, format_diff(attribute, value, actual_value))
51
+ full_name = parent_prefix.length > 0 ? "#{parent_prefix}.#{name}" : name
45
52
 
46
- else
47
- attribute, actual_value, expected_value = operands
53
+ if non_empty_hash?(removed_value) && non_empty_hash?(added_value)
54
+ add_diff_to_message(removed_value, added_value, full_name)
55
+
56
+ elsif non_empty_array?(removed_value) && non_empty_array?(added_value)
48
57
 
49
- add_diff_description(attribute, format_diff(attribute, expected_value, actual_value))
58
+ [removed_value.length, added_value.length].max.times do |i|
59
+ add_diff_to_message(removed_value[i], added_value[i], full_name)
60
+ end
61
+ else
62
+ 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
68
+ )
69
+ )
70
+ 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)
76
+ )
77
+ )
78
+ end
50
79
  end
80
+
51
81
  end
82
+ end
52
83
 
53
- @message
84
+ def non_empty_hash?(target)
85
+ target.kind_of?(Hash) && target.any?
54
86
  end
55
87
 
56
- private
88
+ def non_empty_array?(target)
89
+ target.kind_of?(Array) && target.any?
90
+ end
57
91
 
58
92
  def add_diff_description(attribute, difference_description)
59
93
  unless already_reported_difference?(attribute)
@@ -79,10 +113,13 @@ module TestAssistant::Json
79
113
 
80
114
  result = target
81
115
 
116
+
82
117
  keys.each do |key|
118
+
83
119
  unless key == ''
84
120
  result = result[key]
85
121
  end
122
+
86
123
  end
87
124
 
88
125
  result
@@ -1,3 +1,3 @@
1
1
  module TestAssistant
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,369 @@
1
+ require 'test_assistant/email/helpers'
2
+ require_relative './support/email_mock'
3
+
4
+ RSpec.describe 'have_sent_email' do
5
+ include TestAssistant::Email::Helpers
6
+
7
+ context "when no emails have been sent" do
8
+ subject { [] }
9
+
10
+ it "then the positive assertion fails" do
11
+ expect {
12
+ expect(subject).to have_been_sent
13
+ }.to raise_error.with_message('Expected an email to be sent. However, no emails were sent.')
14
+ end
15
+
16
+ it "then the negative assertion passes" do
17
+ expect {
18
+ expect(subject).to_not have_been_sent
19
+ }.to_not raise_error
20
+ end
21
+
22
+ it "then a non-matching 'to' assertion fails" do
23
+ expect {
24
+ expect(subject).to have_been_sent.to('test@email.com')
25
+ }.to raise_error.with_message('Expected an email to be sent to \'test@email.com\'. However, no emails were sent.')
26
+ end
27
+
28
+ it "then a non-matching 'from' assertion fails" do
29
+ expect {
30
+ expect(subject).to have_been_sent.from('test@email.com')
31
+ }.to raise_error.with_message('Expected an email to be sent from \'test@email.com\'. However, no emails were sent.')
32
+ end
33
+
34
+ it "then a non-matching 'with_subject' assertion fails" do
35
+ expect {
36
+ expect(subject).to have_been_sent.with_subject('Subject')
37
+ }.to raise_error.with_message('Expected an email to be sent with subject \'Subject\'. However, no emails were sent.')
38
+ end
39
+
40
+ it "then a non-matching 'with_text' assertion fails" do
41
+ expect {
42
+ expect(subject).to have_been_sent.with_text('Text')
43
+ }.to raise_error.with_message('Expected an email to be sent with text \'Text\'. However, no emails were sent.')
44
+ end
45
+
46
+ it "then a non-matching 'matching_selector' assertion fails" do
47
+ expect {
48
+ expect(subject).to have_been_sent.matching_selector('h1')
49
+ }.to raise_error.with_message('Expected an email to be sent matching selector \'h1\'. However, no emails were sent.')
50
+ end
51
+
52
+ it "then a non-matching 'with_link' assertion fails" do
53
+ expect {
54
+ expect(subject).to have_been_sent.with_link('www.example.com')
55
+ }.to raise_error.with_message('Expected an email to be sent with link \'www.example.com\'. However, no emails were sent.')
56
+ end
57
+
58
+ it "then a non-matching 'with_image' assertion fails" do
59
+ expect {
60
+ expect(subject).to have_been_sent.with_image('www.example.com')
61
+ }.to raise_error.with_message('Expected an email to be sent with image \'www.example.com\'. However, no emails were sent.')
62
+ end
63
+
64
+ end
65
+
66
+ context "when an email has been sent" do
67
+ subject { [ EmailMock.new ] }
68
+
69
+ it "then the unqualified assertion passes" do
70
+ expect {
71
+ expect(subject).to have_been_sent
72
+ }.to_not raise_error
73
+ end
74
+
75
+ it "then the unqualified negative assertion fails" do
76
+ expect {
77
+ expect(subject).to_not have_been_sent
78
+ }.to raise_error("Expected no emails to be sent.")
79
+ end
80
+ end
81
+
82
+ context "when a matching email has been sent" do
83
+ subject { [ EmailMock.new ] }
84
+
85
+ it "then a positive 'to' assertion passes" do
86
+ expect {
87
+ expect(subject).to have_been_sent.to(subject[0].to)
88
+ }.to_not raise_error
89
+ end
90
+
91
+ it "then a negative 'to' assertion fails" do
92
+ expect {
93
+ expect(subject).to_not have_been_sent.to(subject[0].to)
94
+ }.to raise_error.with_message("Expected no emails to be sent to '#{subject[0].to}'.")
95
+ end
96
+
97
+ it "then a positive 'from' assertion passes" do
98
+ expect {
99
+ expect(subject).to have_been_sent.from(subject[0].from)
100
+ }.to_not raise_error
101
+ end
102
+
103
+ it "then a negative 'from' assertion fails" do
104
+ expect {
105
+ expect(subject).to_not have_been_sent.from(subject[0].from)
106
+ }.to raise_error.with_message("Expected no emails to be sent from '#{subject[0].from}'.")
107
+ end
108
+
109
+ it "then a positive 'with_subject' assertion passes" do
110
+ expect {
111
+ expect(subject).to have_been_sent.with_subject(subject[0].subject)
112
+ }.to_not raise_error
113
+ end
114
+
115
+ it "then a negative 'with_subject' assertion fails" do
116
+ expect {
117
+ expect(subject).to_not have_been_sent.with_subject(subject[0].subject)
118
+ }.to raise_error.with_message("Expected no emails to be sent with subject '#{subject[0].subject}'.")
119
+ end
120
+
121
+ it "then a positive 'with_text' assertion passes" do
122
+ expect {
123
+ expect(subject).to have_been_sent.with_text(subject[0].text)
124
+ }.to_not raise_error
125
+ end
126
+
127
+ it "then a negative 'with_text' assertion fails" do
128
+ expect {
129
+ expect(subject).to_not have_been_sent.with_text(subject[0].text)
130
+ }.to raise_error.with_message("Expected no emails to be sent with text '#{subject[0].text}'.")
131
+ end
132
+
133
+ it "then a positive 'matching_selector' assertion passes" do
134
+ expect {
135
+ expect(subject).to have_been_sent.matching_selector('h1')
136
+ }.to_not raise_error
137
+ end
138
+
139
+ it "then a negative 'matching_selector' assertion fails" do
140
+ expect {
141
+ expect(subject).to_not have_been_sent.matching_selector('h1')
142
+ }.to raise_error.with_message("Expected no emails to be sent matching selector 'h1'.")
143
+ end
144
+
145
+ it "then a positive 'with_link' assertion passes" do
146
+ expect {
147
+ expect(subject).to have_been_sent.with_link('www.test.com')
148
+ }.to_not raise_error
149
+ end
150
+
151
+ it "then a negative 'with_link' assertion fails" do
152
+ expect {
153
+ expect(subject).to_not have_been_sent.with_link('www.test.com')
154
+ }.to raise_error.with_message("Expected no emails to be sent with link 'www.test.com'.")
155
+ end
156
+
157
+ it "then a positive 'with_image' assertion passes" do
158
+ expect {
159
+ expect(subject).to have_been_sent.with_image('www.test.com')
160
+ }.to_not raise_error
161
+ end
162
+
163
+ it "then a negative 'with_image' assertion fails" do
164
+ expect {
165
+ expect(subject).to_not have_been_sent.with_image('www.test.com')
166
+ }.to raise_error.with_message("Expected no emails to be sent with image 'www.test.com'.")
167
+ end
168
+ end
169
+
170
+ context "when a non-matching email has been sent" do
171
+ subject { [ EmailMock.new ] }
172
+
173
+ it "then a positive 'to' assertion fails" do
174
+ expect {
175
+ expect(subject).to have_been_sent.to('other@email.com')
176
+ }.to raise_error.with_message("Expected an email to be sent to 'other@email.com'. However, 1 was sent to '#{subject[0].to}'.")
177
+ end
178
+
179
+ it "then a negative 'to' assertion passes" do
180
+ expect {
181
+ expect(subject).to_not have_been_sent.to('other@email.com')
182
+ }.to_not raise_error
183
+ end
184
+
185
+ it "then a positive 'from' assertion fails" do
186
+ expect {
187
+ expect(subject).to have_been_sent.from('other@email.com')
188
+ }.to raise_error.with_message("Expected an email to be sent from 'other@email.com'. However, 1 was sent from '#{subject[0].from}'.")
189
+ end
190
+
191
+ it "then a negative 'from' assertion passes" do
192
+ expect {
193
+ expect(subject).to_not have_been_sent.from('other@email.com')
194
+ }.to_not raise_error
195
+ end
196
+
197
+ it "then a positive 'with_subject' assertion fails" do
198
+ expect {
199
+ expect(subject).to have_been_sent.with_subject('Other Subject')
200
+ }.to raise_error.with_message("Expected an email to be sent with subject 'Other Subject'. However, 1 was sent with subject '#{subject[0].subject}'.")
201
+ end
202
+
203
+ it "then a negative 'with_subject' assertion passes" do
204
+ expect {
205
+ expect(subject).to_not have_been_sent.with_subject('Other Subject')
206
+ }.to_not raise_error
207
+ end
208
+
209
+ it "then a positive 'with_text' assertion fails" do
210
+ expect {
211
+ expect(subject).to have_been_sent.with_text('Other text')
212
+ }.to raise_error.with_message("Expected an email to be sent with text 'Other text'. However, 1 was sent with text '#{subject[0].text}'.")
213
+ end
214
+
215
+ it "then a negative 'with_text' assertion passes" do
216
+ expect {
217
+ expect(subject).to_not have_been_sent.with_text('Other text')
218
+ }.to_not raise_error
219
+ end
220
+
221
+ it "then a positive 'matching_selector' assertion fails" do
222
+ expect {
223
+ expect(subject).to have_been_sent.matching_selector('.other')
224
+ }.to raise_error.with_message("Expected an email to be sent matching selector '.other'. However, 1 was sent with body #{subject[0].body}.")
225
+ end
226
+
227
+ it "then a negative 'matching_selector' assertion passes" do
228
+ expect {
229
+ expect(subject).to_not have_been_sent.matching_selector('.other')
230
+ }.to_not raise_error
231
+ end
232
+
233
+ it "then a positive 'with_link' assertion fails"do
234
+ expect {
235
+ expect(subject).to have_been_sent.with_link('www.other.com')
236
+ }.to raise_error.with_message("Expected an email to be sent with link 'www.other.com'. However, 1 was sent with body #{subject[0].body}.")
237
+ end
238
+
239
+ it "then a negative 'with_link' assertion passes" do
240
+ expect {
241
+ expect(subject).to_not have_been_sent.with_link('www.other.com')
242
+ }.to_not raise_error
243
+ end
244
+
245
+ it "then a positive 'with_image' assertion fails" do
246
+ expect {
247
+ expect(subject).to have_been_sent.with_image('www.other.com')
248
+ }.to raise_error.with_message("Expected an email to be sent with image 'www.other.com'. However, 1 was sent with body #{subject[0].body}.")
249
+ end
250
+
251
+ it "then a negative 'with_image' assertion passes" do
252
+ expect {
253
+ expect(subject).to_not have_been_sent.with_image('www.other.com')
254
+ }.to_not raise_error
255
+ end
256
+ end
257
+
258
+ context "when multiple emails have been sent" do
259
+ subject { [ EmailMock.new, EmailMock.new(to: 'other@email.com') ] }
260
+
261
+ it "then a positive assertion matching the first email passes" do
262
+ expect {
263
+ expect(subject).to have_been_sent.to(subject[0].to)
264
+ }.to_not raise_error
265
+ end
266
+
267
+ it "then a negative assertion matching the first email fails" do
268
+ expect {
269
+ expect(subject).to_not have_been_sent.to(subject[0].to)
270
+ }.to raise_error.with_message("Expected no emails to be sent to '#{subject[0].to}'.")
271
+ end
272
+
273
+ it "then a positive assertion matching the second email passes" do
274
+ expect {
275
+ expect(subject).to have_been_sent.to(subject[1].to)
276
+ }.to_not raise_error
277
+ end
278
+
279
+ it "then a negative assertion matching the second email fails" do
280
+ expect {
281
+ expect(subject).to_not have_been_sent.to(subject[1].to)
282
+ }.to raise_error.with_message("Expected no emails to be sent to '#{subject[1].to}'.")
283
+ end
284
+
285
+ end
286
+
287
+ context "when using multiple qualifiers" do
288
+ subject { [ EmailMock.new ] }
289
+
290
+ it "then a positive assertions correctly matches a matching email" do
291
+ expect {
292
+ expect(subject).to have_been_sent.to(subject[0].to).from(subject[0].from)
293
+ }.to_not raise_error
294
+ end
295
+
296
+ it "then a positive assertions don't match an email if the first qualifier isn't satisfied" do
297
+ expect {
298
+ expect(subject).to have_been_sent.to('other@email.com').from(subject[0].from)
299
+ }.to raise_error.with_message("Expected an email to be sent to 'other@email.com'. However, 1 was sent to '#{subject[0].to}'.")
300
+ end
301
+
302
+ it "then a positive assertions don't match an email if the last qualifier isn't satisfied" do
303
+ expect {
304
+ expect(subject).to have_been_sent.to(subject[0].to).from('other@email.com')
305
+ }.to raise_error.with_message("Expected an email to be sent from 'other@email.com'. However, 1 was sent from '#{subject[0].from}'.")
306
+ end
307
+
308
+ it "then a negative assertions correctly matches a matching email" do
309
+ expect {
310
+ expect(subject).to_not have_been_sent.to(subject[0].to).from(subject[0].from)
311
+ }.to raise_error.with_message("Expected no emails to be sent to '#{subject[0].to}' from '#{subject[0].from}'.")
312
+ end
313
+
314
+ it "then a negative assertions don't match an email if the first qualifier isn't satisfied" do
315
+ expect {
316
+ expect(subject).to_not have_been_sent.to('other@email.com').from(subject[0].from)
317
+ }.to_not raise_error
318
+ end
319
+
320
+ it "then a negative assertions don't match an email if the last qualifier isn't satisfied" do
321
+ expect {
322
+ expect(subject).to_not have_been_sent.to(subject[0].to).from('other@email.com')
323
+ }.to_not raise_error
324
+ end
325
+ end
326
+
327
+ context "when using the and method" do
328
+ subject { [ EmailMock.new ] }
329
+
330
+ it "then a positive assertion will fail if the first qualifier is not satisfied" do
331
+ expect {
332
+ expect(subject).to have_been_sent.with_text('Other').and('Email')
333
+ }.to raise_error.with_message("Expected an email to be sent with text 'Other' and 'Email'. However, 1 was sent with text '#{subject[0].text}'.")
334
+ end
335
+
336
+ it "then a positive assertion will fail if the second qualifier is not satisfied" do
337
+ expect {
338
+ expect(subject).to have_been_sent.with_text('Test').and('Other')
339
+ }.to raise_error.with_message("Expected an email to be sent with text 'Test' and 'Other'. However, 1 was sent with text '#{subject[0].text}'.")
340
+ end
341
+
342
+ it "then a positive assertion will pass if both qualifiers are satisfied" do
343
+ expect {
344
+ expect(subject).to have_been_sent.with_text('Test').and('Email')
345
+ }.to_not raise_error
346
+ end
347
+
348
+ it "then a negative assertion will pass if the first qualifier is not satisfied" do
349
+ expect {
350
+ expect(subject).to_not have_been_sent.with_text('Other').and('Email')
351
+ }.to_not raise_error
352
+ end
353
+
354
+ it "then a negative assertion will pass if the second qualifier is not satisfied" do
355
+ expect {
356
+ expect(subject).to_not have_been_sent.with_text('Test').and('Other')
357
+ }.to_not raise_error
358
+ end
359
+
360
+ it "then a negative assertion will fail if both qualifiers are satisfied" do
361
+ expect {
362
+ expect(subject).to_not have_been_sent.with_text('Test').and('Email')
363
+ }.to raise_error.with_message('Expected no emails to be sent with text \'Test\' and \'Email\'.')
364
+ end
365
+
366
+ end
367
+
368
+
369
+ end
@@ -104,6 +104,64 @@ RSpec.describe "eql_json" do
104
104
 
105
105
  end
106
106
 
107
+ context "when comparing arrays of objects" do
108
+ let(:expected) {
109
+ {
110
+ 'alpha' => 'alpha',
111
+ 'beta' => [ 1, 2, 3],
112
+ 'gamma' => [
113
+ { 'i' => 'a', 'j' => 'b' },
114
+ { 'i' => 'c', 'j' => 'd' },
115
+ { 'i' => 'e', 'j' => 'f' },
116
+ ]
117
+ }
118
+ }
119
+
120
+ let(:actual) {
121
+ {
122
+ 'alpha' => 'alpha',
123
+ 'beta' => [ 1, 2, 3],
124
+ 'gamma' => [
125
+ { 'j' => 'b' },
126
+ { 'i' => 'c', 'j' => 'D' },
127
+ { 'i' => 'e', 'j' => 'f', 'k' => 'k' },
128
+ ]
129
+ }
130
+ }
131
+
132
+ it "then correctly reports the elements that have changed" do
133
+
134
+ expect(actual).to eql(actual)
135
+
136
+ expect(actual).to_not eql(expected)
137
+
138
+ begin
139
+ expect(actual).to eql_json(expected)
140
+ rescue RSpec::Expectations::ExpectationNotMetError => e
141
+
142
+ expect(e.message).to eql(error_message(expected, actual, {
143
+
144
+ 'gamma[0].i' => {
145
+ expected: "'a'",
146
+ actual: ''
147
+ },
148
+ 'gamma[1].j' => {
149
+ expected: "'d'",
150
+ actual: "'D'"
151
+ },
152
+ 'gamma[2].k' => {
153
+ expected: '',
154
+ actual: "'k'"
155
+ }
156
+
157
+ }))
158
+
159
+ end
160
+
161
+ end
162
+
163
+ end
164
+
107
165
  context "when comparing objects" do
108
166
  let(:expected) { {
109
167
  'a' => 'a',
@@ -0,0 +1,27 @@
1
+ class EmailMock
2
+ attr_reader :to, :from, :subject, :text, :body
3
+
4
+ def initialize(to: 'receiver@email.com', from: 'sender@email.com', subject: 'Subject', text: 'Test Email', body: "<body><h1>Test Email</h1><a href='www.test.com' /><img src='www.test.com' /></body>")
5
+ @to, @from, @subject, @text, @body = to, from, subject, text, body
6
+ end
7
+
8
+ def parts
9
+ [
10
+ EmailBodyMock.new(@body)
11
+ ]
12
+ end
13
+ end
14
+
15
+ class EmailBodyMock
16
+ def initialize(text)
17
+ @text = text
18
+ end
19
+
20
+ def body
21
+ self
22
+ end
23
+
24
+ def decoded
25
+ @text
26
+ end
27
+ 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.0.4
4
+ version: 0.1.0
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-02-27 00:00:00.000000000 Z
11
+ date: 2017-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -137,8 +137,10 @@ files:
137
137
  - lib/test_assistant/json/expectation.rb
138
138
  - lib/test_assistant/json/helpers.rb
139
139
  - lib/test_assistant/version.rb
140
+ - spec/email_expectation_spec.rb
140
141
  - spec/eql_json_spec.rb
141
142
  - spec/spec_helper.rb
143
+ - spec/support/email_mock.rb
142
144
  - test_assistant.gemspec
143
145
  homepage: https://github.com/greena13/test_assistant
144
146
  licenses:
@@ -160,10 +162,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
162
  version: '0'
161
163
  requirements: []
162
164
  rubyforge_project:
163
- rubygems_version: 2.2.2
165
+ rubygems_version: 2.5.1
164
166
  signing_key:
165
167
  specification_version: 4
166
168
  summary: A toolbox for increased testing efficiency with RSpec
167
169
  test_files:
170
+ - spec/email_expectation_spec.rb
168
171
  - spec/eql_json_spec.rb
169
172
  - spec/spec_helper.rb
173
+ - spec/support/email_mock.rb