rspec-rails 3.0.2 → 7.1.1

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.
Files changed (105) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.document +1 -1
  4. data/.yardopts +3 -1
  5. data/Capybara.md +6 -55
  6. data/Changelog.md +805 -47
  7. data/{License.txt → LICENSE.md} +5 -3
  8. data/README.md +278 -444
  9. data/lib/generators/rspec/channel/channel_generator.rb +12 -0
  10. data/lib/generators/rspec/{observer/templates/observer_spec.rb → channel/templates/channel_spec.rb.erb} +1 -1
  11. data/lib/generators/rspec/controller/controller_generator.rb +24 -7
  12. data/lib/generators/rspec/controller/templates/controller_spec.rb +3 -3
  13. data/lib/generators/rspec/controller/templates/request_spec.rb +19 -0
  14. data/lib/generators/rspec/controller/templates/routing_spec.rb +13 -0
  15. data/lib/generators/rspec/controller/templates/view_spec.rb +1 -1
  16. data/lib/generators/rspec/feature/feature_generator.rb +15 -2
  17. data/lib/generators/rspec/feature/templates/feature_singular_spec.rb +5 -0
  18. data/lib/generators/rspec/feature/templates/feature_spec.rb +1 -1
  19. data/lib/generators/rspec/generator/generator_generator.rb +24 -0
  20. data/lib/generators/rspec/generator/templates/generator_spec.rb +5 -0
  21. data/lib/generators/rspec/helper/helper_generator.rb +2 -2
  22. data/lib/generators/rspec/helper/templates/helper_spec.rb +1 -1
  23. data/lib/generators/rspec/install/install_generator.rb +41 -7
  24. data/lib/generators/rspec/install/templates/spec/rails_helper.rb +63 -22
  25. data/lib/generators/rspec/job/job_generator.rb +13 -0
  26. data/lib/generators/rspec/job/templates/job_spec.rb.erb +7 -0
  27. data/lib/generators/rspec/mailbox/mailbox_generator.rb +14 -0
  28. data/lib/generators/rspec/mailbox/templates/mailbox_spec.rb.erb +7 -0
  29. data/lib/generators/rspec/mailer/mailer_generator.rb +12 -3
  30. data/lib/generators/rspec/mailer/templates/mailer_spec.rb +2 -2
  31. data/lib/generators/rspec/mailer/templates/preview.rb +13 -0
  32. data/lib/generators/rspec/model/model_generator.rb +20 -6
  33. data/lib/generators/rspec/model/templates/fixtures.yml +1 -1
  34. data/lib/generators/rspec/model/templates/model_spec.rb +1 -1
  35. data/lib/generators/rspec/request/request_generator.rb +17 -0
  36. data/lib/generators/rspec/request/templates/request_spec.rb +10 -0
  37. data/lib/generators/rspec/scaffold/scaffold_generator.rb +90 -113
  38. data/lib/generators/rspec/scaffold/templates/api_controller_spec.rb +129 -0
  39. data/lib/generators/rspec/scaffold/templates/api_request_spec.rb +131 -0
  40. data/lib/generators/rspec/scaffold/templates/controller_spec.rb +46 -64
  41. data/lib/generators/rspec/scaffold/templates/edit_spec.rb +11 -7
  42. data/lib/generators/rspec/scaffold/templates/index_spec.rb +4 -3
  43. data/lib/generators/rspec/scaffold/templates/new_spec.rb +4 -4
  44. data/lib/generators/rspec/scaffold/templates/request_spec.rb +138 -0
  45. data/lib/generators/rspec/scaffold/templates/routing_spec.rb +18 -11
  46. data/lib/generators/rspec/scaffold/templates/show_spec.rb +3 -3
  47. data/lib/generators/rspec/system/system_generator.rb +24 -0
  48. data/lib/generators/rspec/system/templates/system_spec.rb +9 -0
  49. data/lib/generators/rspec/view/templates/view_spec.rb +1 -1
  50. data/lib/generators/rspec/view/view_generator.rb +4 -4
  51. data/lib/generators/rspec.rb +30 -11
  52. data/lib/rspec/rails/active_record.rb +25 -0
  53. data/lib/rspec/rails/adapters.rb +46 -29
  54. data/lib/rspec/rails/configuration.rb +165 -41
  55. data/lib/rspec/rails/example/channel_example_group.rb +93 -0
  56. data/lib/rspec/rails/example/controller_example_group.rb +185 -149
  57. data/lib/rspec/rails/example/feature_example_group.rb +43 -23
  58. data/lib/rspec/rails/example/helper_example_group.rb +28 -25
  59. data/lib/rspec/rails/example/job_example_group.rb +23 -0
  60. data/lib/rspec/rails/example/mailbox_example_group.rb +80 -0
  61. data/lib/rspec/rails/example/mailer_example_group.rb +27 -22
  62. data/lib/rspec/rails/example/model_example_group.rb +9 -6
  63. data/lib/rspec/rails/example/rails_example_group.rb +9 -2
  64. data/lib/rspec/rails/example/request_example_group.rb +21 -17
  65. data/lib/rspec/rails/example/routing_example_group.rb +47 -39
  66. data/lib/rspec/rails/example/system_example_group.rb +180 -0
  67. data/lib/rspec/rails/example/view_example_group.rb +179 -134
  68. data/lib/rspec/rails/example.rb +4 -0
  69. data/lib/rspec/rails/extensions/active_record/proxy.rb +5 -11
  70. data/lib/rspec/rails/feature_check.rb +51 -0
  71. data/lib/rspec/rails/file_fixture_support.rb +18 -0
  72. data/lib/rspec/rails/fixture_file_upload_support.rb +45 -0
  73. data/lib/rspec/rails/fixture_support.rb +70 -14
  74. data/lib/rspec/rails/matchers/action_cable/have_broadcasted_to.rb +180 -0
  75. data/lib/rspec/rails/matchers/action_cable/have_streams.rb +58 -0
  76. data/lib/rspec/rails/matchers/action_cable.rb +70 -0
  77. data/lib/rspec/rails/matchers/action_mailbox.rb +73 -0
  78. data/lib/rspec/rails/matchers/active_job.rb +526 -0
  79. data/lib/rspec/rails/matchers/base_matcher.rb +179 -0
  80. data/lib/rspec/rails/matchers/be_a_new.rb +70 -64
  81. data/lib/rspec/rails/matchers/be_new_record.rb +25 -20
  82. data/lib/rspec/rails/matchers/be_valid.rb +39 -34
  83. data/lib/rspec/rails/matchers/have_enqueued_mail.rb +259 -0
  84. data/lib/rspec/rails/matchers/have_http_status.rb +359 -333
  85. data/lib/rspec/rails/matchers/have_rendered.rb +55 -32
  86. data/lib/rspec/rails/matchers/redirect_to.rb +30 -27
  87. data/lib/rspec/rails/matchers/relation_match_array.rb +1 -1
  88. data/lib/rspec/rails/matchers/routing_matchers.rb +107 -101
  89. data/lib/rspec/rails/matchers/send_email.rb +122 -0
  90. data/lib/rspec/rails/matchers.rb +21 -12
  91. data/lib/rspec/rails/tasks/rspec.rake +9 -17
  92. data/lib/rspec/rails/vendor/capybara.rb +10 -11
  93. data/lib/rspec/rails/version.rb +1 -1
  94. data/lib/rspec/rails/view_assigns.rb +1 -20
  95. data/lib/rspec/rails/view_path_builder.rb +29 -0
  96. data/lib/rspec/rails/view_rendering.rb +89 -27
  97. data/lib/rspec/rails/view_spec_methods.rb +56 -0
  98. data/lib/rspec/rails.rb +9 -1
  99. data/lib/rspec-rails.rb +83 -3
  100. data.tar.gz.sig +0 -0
  101. metadata +108 -78
  102. metadata.gz.sig +3 -2
  103. data/lib/generators/rspec/integration/integration_generator.rb +0 -17
  104. data/lib/generators/rspec/integration/templates/request_spec.rb +0 -10
  105. data/lib/generators/rspec/observer/observer_generator.rb +0 -13
@@ -0,0 +1,526 @@
1
+ require "active_job/base"
2
+ require "active_job/arguments"
3
+
4
+ module RSpec
5
+ module Rails
6
+ module Matchers
7
+ # Namespace for various implementations of ActiveJob features
8
+ #
9
+ # @api private
10
+ module ActiveJob
11
+ # rubocop: disable Metrics/ClassLength
12
+ # @private
13
+ class Base < RSpec::Rails::Matchers::BaseMatcher
14
+ def initialize
15
+ @args = []
16
+ @queue = nil
17
+ @priority = nil
18
+ @at = nil
19
+ @block = proc { }
20
+ set_expected_number(:exactly, 1)
21
+ end
22
+
23
+ def with(*args, &block)
24
+ @args = args
25
+ @block = block if block.present?
26
+ self
27
+ end
28
+
29
+ def on_queue(queue)
30
+ @queue = queue.to_s
31
+ self
32
+ end
33
+
34
+ def at_priority(priority)
35
+ @priority = priority.to_i
36
+ self
37
+ end
38
+
39
+ def at(time_or_date)
40
+ case time_or_date
41
+ when Time then @at = Time.at(time_or_date.to_f)
42
+ else
43
+ @at = time_or_date
44
+ end
45
+ self
46
+ end
47
+
48
+ def exactly(count)
49
+ set_expected_number(:exactly, count)
50
+ self
51
+ end
52
+
53
+ def at_least(count)
54
+ set_expected_number(:at_least, count)
55
+ self
56
+ end
57
+
58
+ def at_most(count)
59
+ set_expected_number(:at_most, count)
60
+ self
61
+ end
62
+
63
+ def times
64
+ self
65
+ end
66
+
67
+ def once
68
+ exactly(:once)
69
+ end
70
+
71
+ def twice
72
+ exactly(:twice)
73
+ end
74
+
75
+ def thrice
76
+ exactly(:thrice)
77
+ end
78
+
79
+ def failure_message
80
+ return @failure_message if defined?(@failure_message)
81
+
82
+ "expected to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION} #{base_message}".tap do |msg|
83
+ if @unmatching_jobs.any?
84
+ msg << "\nQueued jobs:"
85
+ @unmatching_jobs.each do |job|
86
+ msg << "\n #{base_job_message(job)}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def failure_message_when_negated
93
+ "expected not to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION} #{base_message}"
94
+ end
95
+
96
+ def message_expectation_modifier
97
+ case @expectation_type
98
+ when :exactly then "exactly"
99
+ when :at_most then "at most"
100
+ when :at_least then "at least"
101
+ end
102
+ end
103
+
104
+ def supports_block_expectations?
105
+ true
106
+ end
107
+
108
+ private
109
+
110
+ def check(jobs)
111
+ @matching_jobs, @unmatching_jobs = jobs.partition do |job|
112
+ if matches_constraints?(job)
113
+ args = deserialize_arguments(job)
114
+ @block.call(*args)
115
+ true
116
+ else
117
+ false
118
+ end
119
+ end
120
+
121
+ if (signature_mismatch = detect_args_signature_mismatch(@matching_jobs))
122
+ @failure_message = signature_mismatch
123
+ return false
124
+ end
125
+
126
+ @matching_jobs_count = @matching_jobs.size
127
+
128
+ case @expectation_type
129
+ when :exactly then @expected_number == @matching_jobs_count
130
+ when :at_most then @expected_number >= @matching_jobs_count
131
+ when :at_least then @expected_number <= @matching_jobs_count
132
+ end
133
+ end
134
+
135
+ def base_message
136
+ "#{message_expectation_modifier} #{@expected_number} jobs,".tap do |msg|
137
+ msg << " with #{@args}," if @args.any?
138
+ msg << " on queue #{@queue}," if @queue
139
+ msg << " at #{@at.inspect}," if @at
140
+ msg << " with priority #{@priority}," if @priority
141
+ msg << " but #{self.class::MESSAGE_EXPECTATION_ACTION} #{@matching_jobs_count}"
142
+ end
143
+ end
144
+
145
+ def base_job_message(job)
146
+ msg_parts = []
147
+ msg_parts << "with #{deserialize_arguments(job)}" if job[:args].any?
148
+ msg_parts << "on queue #{job[:queue]}" if job[:queue]
149
+ msg_parts << "at #{Time.at(job[:at])}" if job[:at]
150
+ msg_parts <<
151
+ if job[:priority]
152
+ "with priority #{job[:priority]}"
153
+ else
154
+ "with no priority specified"
155
+ end
156
+
157
+ "#{job[:job].name} job".tap do |msg|
158
+ msg << " #{msg_parts.join(', ')}" if msg_parts.any?
159
+ end
160
+ end
161
+
162
+ def matches_constraints?(job)
163
+ job_matches?(job) && arguments_match?(job) && queue_match?(job) && at_match?(job) && priority_match?(job)
164
+ end
165
+
166
+ def job_matches?(job)
167
+ @job ? @job == job[:job] : true
168
+ end
169
+
170
+ def arguments_match?(job)
171
+ if @args.any?
172
+ args = serialize_and_deserialize_arguments(@args)
173
+ deserialized_args = deserialize_arguments(job)
174
+ RSpec::Mocks::ArgumentListMatcher.new(*args).args_match?(*deserialized_args)
175
+ else
176
+ true
177
+ end
178
+ end
179
+
180
+ def detect_args_signature_mismatch(jobs)
181
+ return if skip_signature_verification?
182
+
183
+ jobs.each do |job|
184
+ args = deserialize_arguments(job)
185
+
186
+ if (signature_mismatch = check_args_signature_mismatch(job.fetch(:job), :perform, args))
187
+ return signature_mismatch
188
+ end
189
+ end
190
+
191
+ nil
192
+ end
193
+
194
+ def skip_signature_verification?
195
+ return true unless defined?(::RSpec::Mocks) && (::RSpec::Mocks.respond_to?(:configuration))
196
+
197
+ !RSpec::Mocks.configuration.verify_partial_doubles? ||
198
+ RSpec::Mocks.configuration.temporarily_suppress_partial_double_verification
199
+ end
200
+
201
+ def check_args_signature_mismatch(job_class, job_method, args)
202
+ signature = Support::MethodSignature.new(job_class.public_instance_method(job_method))
203
+ verifier = Support::StrictSignatureVerifier.new(signature, args)
204
+
205
+ unless verifier.valid?
206
+ "Incorrect arguments passed to #{job_class.name}: #{verifier.error_message}"
207
+ end
208
+ end
209
+
210
+ def queue_match?(job)
211
+ return true unless @queue
212
+
213
+ @queue == job[:queue]
214
+ end
215
+
216
+ def priority_match?(job)
217
+ return true unless @priority
218
+
219
+ @priority == job[:priority]
220
+ end
221
+
222
+ def at_match?(job)
223
+ return true unless @at
224
+ return job[:at].nil? if @at == :no_wait
225
+ return false unless job[:at]
226
+
227
+ scheduled_at = Time.at(job[:at])
228
+ values_match?(@at, scheduled_at) || check_for_inprecise_value(scheduled_at)
229
+ end
230
+
231
+ def check_for_inprecise_value(scheduled_at)
232
+ return unless Time === @at && values_match?(@at.change(usec: 0), scheduled_at)
233
+
234
+ RSpec.warn_with((<<-WARNING).gsub(/^\s+\|/, '').chomp)
235
+ |[WARNING] Your expected `at(...)` value does not match the job scheduled_at value
236
+ |unless microseconds are removed. This precision error often occurs when checking
237
+ |values against `Time.current` / `Time.now` which have usec precision, but Rails
238
+ |uses `n.seconds.from_now` internally which has a usec count of `0`.
239
+ |
240
+ |Use `change(usec: 0)` to correct these values. For example:
241
+ |
242
+ |`Time.current.change(usec: 0)`
243
+ |
244
+ |Note: RSpec cannot do this for you because jobs can be scheduled with usec
245
+ |precision and we do not know whether it is on purpose or not.
246
+ |
247
+ |
248
+ WARNING
249
+ false
250
+ end
251
+
252
+ def set_expected_number(relativity, count)
253
+ @expectation_type = relativity
254
+ @expected_number = case count
255
+ when :once then 1
256
+ when :twice then 2
257
+ when :thrice then 3
258
+ else Integer(count)
259
+ end
260
+ end
261
+
262
+ def serialize_and_deserialize_arguments(args)
263
+ serialized = ::ActiveJob::Arguments.serialize(args)
264
+ ::ActiveJob::Arguments.deserialize(serialized)
265
+ rescue ::ActiveJob::SerializationError
266
+ args
267
+ end
268
+
269
+ def deserialize_arguments(job)
270
+ ::ActiveJob::Arguments.deserialize(job[:args])
271
+ rescue ::ActiveJob::DeserializationError
272
+ job[:args]
273
+ end
274
+
275
+ def queue_adapter
276
+ ::ActiveJob::Base.queue_adapter
277
+ end
278
+ end
279
+ # rubocop: enable Metrics/ClassLength
280
+
281
+ # @private
282
+ class HaveEnqueuedJob < Base
283
+ FAILURE_MESSAGE_EXPECTATION_ACTION = 'enqueue'.freeze
284
+ MESSAGE_EXPECTATION_ACTION = 'enqueued'.freeze
285
+
286
+ def initialize(job)
287
+ super()
288
+ @job = job
289
+ end
290
+
291
+ def matches?(proc)
292
+ raise ArgumentError, "have_enqueued_job and enqueue_job only support block expectations" unless Proc === proc
293
+
294
+ original_enqueued_jobs = Set.new(queue_adapter.enqueued_jobs)
295
+ proc.call
296
+ enqueued_jobs = Set.new(queue_adapter.enqueued_jobs)
297
+
298
+ check(enqueued_jobs - original_enqueued_jobs)
299
+ end
300
+
301
+ def does_not_match?(proc)
302
+ set_expected_number(:at_least, 1)
303
+
304
+ !matches?(proc)
305
+ end
306
+ end
307
+
308
+ # @private
309
+ class HaveBeenEnqueued < Base
310
+ FAILURE_MESSAGE_EXPECTATION_ACTION = 'enqueue'.freeze
311
+ MESSAGE_EXPECTATION_ACTION = 'enqueued'.freeze
312
+
313
+ def matches?(job)
314
+ @job = job
315
+ check(queue_adapter.enqueued_jobs)
316
+ end
317
+
318
+ def does_not_match?(proc)
319
+ set_expected_number(:at_least, 1)
320
+
321
+ !matches?(proc)
322
+ end
323
+ end
324
+
325
+ # @private
326
+ class HavePerformedJob < Base
327
+ FAILURE_MESSAGE_EXPECTATION_ACTION = 'perform'.freeze
328
+ MESSAGE_EXPECTATION_ACTION = 'performed'.freeze
329
+
330
+ def initialize(job)
331
+ super()
332
+ @job = job
333
+ end
334
+
335
+ def matches?(proc)
336
+ raise ArgumentError, "have_performed_job only supports block expectations" unless Proc === proc
337
+
338
+ original_performed_jobs_count = queue_adapter.performed_jobs.count
339
+ proc.call
340
+ in_block_jobs = queue_adapter.performed_jobs.drop(original_performed_jobs_count)
341
+
342
+ check(in_block_jobs)
343
+ end
344
+ end
345
+
346
+ # @private
347
+ class HaveBeenPerformed < Base
348
+ FAILURE_MESSAGE_EXPECTATION_ACTION = 'perform'.freeze
349
+ MESSAGE_EXPECTATION_ACTION = 'performed'.freeze
350
+
351
+ def matches?(job)
352
+ @job = job
353
+ check(queue_adapter.performed_jobs)
354
+ end
355
+ end
356
+ end
357
+
358
+ # @api public
359
+ # Passes if a job has been enqueued inside block. May chain at_least, at_most or exactly to specify a number of times.
360
+ #
361
+ # @example
362
+ # expect {
363
+ # HeavyLiftingJob.perform_later
364
+ # }.to have_enqueued_job
365
+ #
366
+ # # Using alias
367
+ # expect {
368
+ # HeavyLiftingJob.perform_later
369
+ # }.to enqueue_job
370
+ #
371
+ # expect {
372
+ # HelloJob.perform_later
373
+ # HeavyLiftingJob.perform_later
374
+ # }.to have_enqueued_job(HelloJob).exactly(:once)
375
+ #
376
+ # expect {
377
+ # 3.times { HelloJob.perform_later }
378
+ # }.to have_enqueued_job(HelloJob).at_least(2).times
379
+ #
380
+ # expect {
381
+ # HelloJob.perform_later
382
+ # }.to have_enqueued_job(HelloJob).at_most(:twice)
383
+ #
384
+ # expect {
385
+ # HelloJob.perform_later
386
+ # HeavyLiftingJob.perform_later
387
+ # }.to have_enqueued_job(HelloJob).and have_enqueued_job(HeavyLiftingJob)
388
+ #
389
+ # expect {
390
+ # HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
391
+ # }.to have_enqueued_job.with(42).on_queue("low").at(Date.tomorrow.noon)
392
+ #
393
+ # expect {
394
+ # HelloJob.set(queue: "low").perform_later(42)
395
+ # }.to have_enqueued_job.with(42).on_queue("low").at(:no_wait)
396
+ #
397
+ # expect {
398
+ # HelloJob.perform_later('rspec_rails', 'rails', 42)
399
+ # }.to have_enqueued_job.with { |from, to, times|
400
+ # # Perform more complex argument matching using dynamic arguments
401
+ # expect(from).to include "_#{to}"
402
+ # }
403
+ def have_enqueued_job(job = nil)
404
+ check_active_job_adapter
405
+ ActiveJob::HaveEnqueuedJob.new(job)
406
+ end
407
+ alias_method :enqueue_job, :have_enqueued_job
408
+
409
+ # @api public
410
+ # Passes if a job has been enqueued. May chain at_least, at_most or exactly to specify a number of times.
411
+ #
412
+ # @example
413
+ # before { ActiveJob::Base.queue_adapter.enqueued_jobs.clear }
414
+ #
415
+ # HeavyLiftingJob.perform_later
416
+ # expect(HeavyLiftingJob).to have_been_enqueued
417
+ #
418
+ # HelloJob.perform_later
419
+ # HeavyLiftingJob.perform_later
420
+ # expect(HeavyLiftingJob).to have_been_enqueued.exactly(:once)
421
+ #
422
+ # 3.times { HelloJob.perform_later }
423
+ # expect(HelloJob).to have_been_enqueued.at_least(2).times
424
+ #
425
+ # HelloJob.perform_later
426
+ # expect(HelloJob).to enqueue_job(HelloJob).at_most(:twice)
427
+ #
428
+ # HelloJob.perform_later
429
+ # HeavyLiftingJob.perform_later
430
+ # expect(HelloJob).to have_been_enqueued
431
+ # expect(HeavyLiftingJob).to have_been_enqueued
432
+ #
433
+ # HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
434
+ # expect(HelloJob).to have_been_enqueued.with(42).on_queue("low").at(Date.tomorrow.noon)
435
+ #
436
+ # HelloJob.set(queue: "low").perform_later(42)
437
+ # expect(HelloJob).to have_been_enqueued.with(42).on_queue("low").at(:no_wait)
438
+ def have_been_enqueued
439
+ check_active_job_adapter
440
+ ActiveJob::HaveBeenEnqueued.new
441
+ end
442
+
443
+ # @api public
444
+ # Passes if a job has been performed inside block. May chain at_least, at_most or exactly to specify a number of times.
445
+ #
446
+ # @example
447
+ # expect {
448
+ # perform_enqueued_jobs { HeavyLiftingJob.perform_later }
449
+ # }.to have_performed_job
450
+ #
451
+ # expect {
452
+ # perform_enqueued_jobs {
453
+ # HelloJob.perform_later
454
+ # HeavyLiftingJob.perform_later
455
+ # }
456
+ # }.to have_performed_job(HelloJob).exactly(:once)
457
+ #
458
+ # expect {
459
+ # perform_enqueued_jobs { 3.times { HelloJob.perform_later } }
460
+ # }.to have_performed_job(HelloJob).at_least(2).times
461
+ #
462
+ # expect {
463
+ # perform_enqueued_jobs { HelloJob.perform_later }
464
+ # }.to have_performed_job(HelloJob).at_most(:twice)
465
+ #
466
+ # expect {
467
+ # perform_enqueued_jobs {
468
+ # HelloJob.perform_later
469
+ # HeavyLiftingJob.perform_later
470
+ # }
471
+ # }.to have_performed_job(HelloJob).and have_performed_job(HeavyLiftingJob)
472
+ #
473
+ # expect {
474
+ # perform_enqueued_jobs {
475
+ # HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
476
+ # }
477
+ # }.to have_performed_job.with(42).on_queue("low").at(Date.tomorrow.noon)
478
+ def have_performed_job(job = nil)
479
+ check_active_job_adapter
480
+ ActiveJob::HavePerformedJob.new(job)
481
+ end
482
+ alias_method :perform_job, :have_performed_job
483
+
484
+ # @api public
485
+ # Passes if a job has been performed. May chain at_least, at_most or exactly to specify a number of times.
486
+ #
487
+ # @example
488
+ # before do
489
+ # ActiveJob::Base.queue_adapter.performed_jobs.clear
490
+ # ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
491
+ # ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
492
+ # end
493
+ #
494
+ # HeavyLiftingJob.perform_later
495
+ # expect(HeavyLiftingJob).to have_been_performed
496
+ #
497
+ # HelloJob.perform_later
498
+ # HeavyLiftingJob.perform_later
499
+ # expect(HeavyLiftingJob).to have_been_performed.exactly(:once)
500
+ #
501
+ # 3.times { HelloJob.perform_later }
502
+ # expect(HelloJob).to have_been_performed.at_least(2).times
503
+ #
504
+ # HelloJob.perform_later
505
+ # HeavyLiftingJob.perform_later
506
+ # expect(HelloJob).to have_been_performed
507
+ # expect(HeavyLiftingJob).to have_been_performed
508
+ #
509
+ # HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
510
+ # expect(HelloJob).to have_been_performed.with(42).on_queue("low").at(Date.tomorrow.noon)
511
+ def have_been_performed
512
+ check_active_job_adapter
513
+ ActiveJob::HaveBeenPerformed.new
514
+ end
515
+
516
+ private
517
+
518
+ # @private
519
+ def check_active_job_adapter
520
+ return if ::ActiveJob::QueueAdapters::TestAdapter === ::ActiveJob::Base.queue_adapter
521
+
522
+ raise StandardError, "To use ActiveJob matchers set `ActiveJob::Base.queue_adapter = :test`"
523
+ end
524
+ end
525
+ end
526
+ end
@@ -0,0 +1,179 @@
1
+ module RSpec
2
+ module Rails
3
+ module Matchers
4
+ # @api private
5
+ #
6
+ # Base class to build matchers. Should not be instantiated directly.
7
+ class BaseMatcher
8
+ include RSpec::Matchers::Composable
9
+
10
+ # @api private
11
+ # Used to detect when no arg is passed to `initialize`.
12
+ # `nil` cannot be used because it's a valid value to pass.
13
+ UNDEFINED = Object.new.freeze
14
+
15
+ # @private
16
+ attr_reader :actual, :expected, :rescued_exception
17
+
18
+ # @private
19
+ attr_writer :matcher_name
20
+
21
+ def initialize(expected = UNDEFINED)
22
+ @expected = expected unless UNDEFINED.equal?(expected)
23
+ end
24
+
25
+ # @api private
26
+ # Indicates if the match is successful. Delegates to `match`, which
27
+ # should be defined on a subclass. Takes care of consistently
28
+ # initializing the `actual` attribute.
29
+ def matches?(actual)
30
+ @actual = actual
31
+ match(expected, actual)
32
+ end
33
+
34
+ # @api private
35
+ # Used to wrap a block of code that will indicate failure by
36
+ # raising one of the named exceptions.
37
+ #
38
+ # This is used by rspec-rails for some of its matchers that
39
+ # wrap rails' assertions.
40
+ def match_unless_raises(*exceptions)
41
+ exceptions.unshift Exception if exceptions.empty?
42
+ begin
43
+ yield
44
+ true
45
+ rescue *exceptions => @rescued_exception
46
+ false
47
+ end
48
+ end
49
+
50
+ # @api private
51
+ # Generates a description using {RSpec::Matchers::EnglishPhrasing}.
52
+ # @return [String]
53
+ def description
54
+ desc = RSpec::Matchers::EnglishPhrasing.split_words(self.class.matcher_name)
55
+ desc << RSpec::Matchers::EnglishPhrasing.list(@expected) if defined?(@expected)
56
+ desc
57
+ end
58
+
59
+ # @api private
60
+ # Matchers are not diffable by default. Override this to make your
61
+ # subclass diffable.
62
+ def diffable?
63
+ false
64
+ end
65
+
66
+ # @api private
67
+ # Most matchers are value matchers (i.e. meant to work with `expect(value)`)
68
+ # rather than block matchers (i.e. meant to work with `expect { }`), so
69
+ # this defaults to false. Block matchers must override this to return true.
70
+ def supports_block_expectations?
71
+ false
72
+ end
73
+
74
+ # @api private
75
+ def expects_call_stack_jump?
76
+ false
77
+ end
78
+
79
+ # @private
80
+ def expected_formatted
81
+ RSpec::Support::ObjectFormatter.format(@expected)
82
+ end
83
+
84
+ # @private
85
+ def actual_formatted
86
+ RSpec::Support::ObjectFormatter.format(@actual)
87
+ end
88
+
89
+ # @private
90
+ def self.matcher_name
91
+ @matcher_name ||= underscore(name.split('::').last)
92
+ end
93
+
94
+ # @private
95
+ def matcher_name
96
+ if defined?(@matcher_name)
97
+ @matcher_name
98
+ else
99
+ self.class.matcher_name
100
+ end
101
+ end
102
+
103
+ # @private
104
+ # Borrowed from ActiveSupport.
105
+ def self.underscore(camel_cased_word)
106
+ word = camel_cased_word.to_s.dup
107
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
108
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
109
+ word.tr!('-', '_')
110
+ word.downcase!
111
+ word
112
+ end
113
+ private_class_method :underscore
114
+
115
+ private
116
+
117
+ def assert_ivars(*expected_ivars)
118
+ return unless (expected_ivars - present_ivars).any?
119
+
120
+ ivar_list = RSpec::Matchers::EnglishPhrasing.list(expected_ivars)
121
+ raise "#{self.class.name} needs to supply#{ivar_list}"
122
+ end
123
+
124
+ alias present_ivars instance_variables
125
+
126
+ # @private
127
+ module HashFormatting
128
+ # `{ :a => 5, :b => 2 }.inspect` produces:
129
+ #
130
+ # {:a=>5, :b=>2}
131
+ #
132
+ # ...but it looks much better as:
133
+ #
134
+ # {:a => 5, :b => 2}
135
+ #
136
+ # This is idempotent and safe to run on a string multiple times.
137
+ def improve_hash_formatting(inspect_string)
138
+ inspect_string.gsub(/(\S)=>(\S)/, '\1 => \2')
139
+ end
140
+ module_function :improve_hash_formatting
141
+ end
142
+
143
+ include HashFormatting
144
+
145
+ # @api private
146
+ # Provides default implementations of failure messages, based on the `description`.
147
+ module DefaultFailureMessages
148
+ # @api private
149
+ # Provides a good generic failure message. Based on `description`.
150
+ # When subclassing, if you are not satisfied with this failure message
151
+ # you often only need to override `description`.
152
+ # @return [String]
153
+ def failure_message
154
+ "expected #{description_of @actual} to #{description}".dup
155
+ end
156
+
157
+ # @api private
158
+ # Provides a good generic negative failure message. Based on `description`.
159
+ # When subclassing, if you are not satisfied with this failure message
160
+ # you often only need to override `description`.
161
+ # @return [String]
162
+ def failure_message_when_negated
163
+ "expected #{description_of @actual} not to #{description}".dup
164
+ end
165
+
166
+ # @private
167
+ def self.has_default_failure_messages?(matcher)
168
+ matcher.method(:failure_message).owner == self &&
169
+ matcher.method(:failure_message_when_negated).owner == self
170
+ rescue NameError
171
+ false
172
+ end
173
+ end
174
+
175
+ include DefaultFailureMessages
176
+ end
177
+ end
178
+ end
179
+ end