Skip to content

Commit a41bba4

Browse files
jdlubranobenoittgt
authored andcommitted
Add have_enqueued mail matcher (#2047)
* Write first test case for calling simple mailer method with deliver_later * Move deliver_later gem logic into HaveEnqueuedMail matcher Moving the internals of https://github.com/jdlubrano/deliver_later_matchers into rspec-rails as a new RSpec::Rails::Matchers::HaveEnqueuedMail class. * Add tests for arguments matcher and error messages. Largely based on existing ActiveJob matcher tests. * Add exactly, at_least, at_most functionality for have_enqueued_mail matcher. * Support mailer methods with default/optional arguments; Support matching email methods that accept parameters when no specific arguments are expected. * Correct private YARD doc on RSpec::Rails::Matchers::HaveEnqueuedJob * Provide unmatching enqueued email info in failure messages The ActiveJob matcher provides output for each unmatching, enqueued job in its failure messages. Those unmatching jobs have certainly helped me catch typos in the past, so it makes sense to provide similar feedback in enqueued email failure messages. * Add tests for have_enqueued_mail matcher at_least and at_most modifiers * Add support for `at` and `on_queue` deliver_later can be called with `wait`, `wait_until`, and `queue` keyword arguments, so the have_enqueued_mail matcher should support matching those types of things. At this point, I am beginning to suspect that this code would be better off using inheritance over composition. I am going to try and refactor to inherit from the ActiveJob base matcher now that I have all of the tests in a good state. * Explicitly require rspec/mocks for access to `anything` method * Ignore Rubocop class length for HaveEnqueuedMail class * Replace .negative? with < 0 for older ruby versions * Remove .inspect call from test to see if that works... * Remove %i array syntax for older ruby versions * Remove the send_time part of the error message to see if that is causing the failed test * Replace new Hash key syntax with old => format for Ruby 1.8.7 * Call strftime in test to get some semblance of timestamp verification * Fix Ruby 1.8.7 syntax errors * Fix ActiveJob::Base.queue_adapter error When using the HaveEnqueuedMail matcher, the error message should not complain about using "ActiveJob" matchers. * Refactor HaveEnqueuedMail matcher to be a subclass of the HaveEnqueuedJob matcher Implementing as a subclass resulted in DRYer code IMO. There was no longer a need to reimplement once, twice, thrice and other duplicated methods that were basically just delegating to the job_matcher instance variable anyways. * Support with blocks * Test that non-mailer jobs do not appear in the have_enqueued_mail matcher list of mail jobs * Make 'deliver_now' a frozen constant * Only pass a mailer-specific block to super if a block is provided * Fix hash rocket syntax for Ruby 1.8.7
1 parent 7ca3d04 commit a41bba4

File tree

2 files changed

+461
-0
lines changed

2 files changed

+461
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
require "rspec/mocks"
2+
require "rspec/rails/matchers/active_job"
3+
4+
module RSpec
5+
module Rails
6+
module Matchers
7+
# Matcher class for `have_enqueued_mail`. Should not be instantiated directly.
8+
#
9+
# rubocop: disable Style/ClassLength
10+
# @private
11+
# @see RSpec::Rails::Matchers#have_enqueued_mail
12+
class HaveEnqueuedMail < ActiveJob::HaveEnqueuedJob
13+
MAILER_JOB_METHOD = 'deliver_now'.freeze
14+
15+
include RSpec::Mocks::ExampleMethods
16+
17+
def initialize(mailer_class, method_name)
18+
super(mailer_job)
19+
@mailer_class = mailer_class
20+
@method_name = method_name
21+
@mail_args = []
22+
@args = mailer_args
23+
end
24+
25+
def description
26+
"enqueues #{@mailer_class.name}.#{@method_name}"
27+
end
28+
29+
def with(*args, &block)
30+
@mail_args = args
31+
block.nil? ? super(*mailer_args) : super(*mailer_args, &yield_mail_args(block))
32+
end
33+
34+
def matches?(block)
35+
raise ArgumentError, 'have_enqueued_mail and enqueue_mail only work with block arguments' unless block.respond_to?(:call)
36+
check_active_job_adapter
37+
super
38+
end
39+
40+
def failure_message
41+
"expected to enqueue #{base_message}".tap do |msg|
42+
msg << "\n#{unmatching_mail_jobs_message}" if unmatching_mail_jobs.any?
43+
end
44+
end
45+
46+
def failure_message_when_negated
47+
"expected not to enqueue #{base_message}"
48+
end
49+
50+
private
51+
52+
def base_message
53+
"#{@mailer_class.name}.#{@method_name}".tap do |msg|
54+
msg << " #{expected_count_message}"
55+
msg << " with #{@mail_args}," if @mail_args.any?
56+
msg << " on queue #{@queue}," if @queue
57+
msg << " at #{@at.inspect}," if @at
58+
msg << " but enqueued #{@matching_jobs.size}"
59+
end
60+
end
61+
62+
def expected_count_message
63+
"#{message_expectation_modifier} #{@expected_number} #{@expected_number == 1 ? 'time' : 'times'}"
64+
end
65+
66+
def mailer_args
67+
if @mail_args.any?
68+
base_mailer_args + @mail_args
69+
else
70+
mailer_method_arity = @mailer_class.instance_method(@method_name).arity
71+
72+
number_of_args = if mailer_method_arity < 0
73+
(mailer_method_arity + 1).abs
74+
else
75+
mailer_method_arity
76+
end
77+
78+
base_mailer_args + Array.new(number_of_args) { anything }
79+
end
80+
end
81+
82+
def base_mailer_args
83+
[@mailer_class.name, @method_name.to_s, MAILER_JOB_METHOD]
84+
end
85+
86+
def yield_mail_args(block)
87+
Proc.new { |*job_args| block.call(*(job_args - base_mailer_args)) }
88+
end
89+
90+
def check_active_job_adapter
91+
return if ::ActiveJob::QueueAdapters::TestAdapter === ::ActiveJob::Base.queue_adapter
92+
raise StandardError, "To use HaveEnqueuedMail matcher set `ActiveJob::Base.queue_adapter = :test`"
93+
end
94+
95+
def unmatching_mail_jobs
96+
@unmatching_jobs.select do |job|
97+
job[:job] == mailer_job
98+
end
99+
end
100+
101+
def unmatching_mail_jobs_message
102+
msg = "Queued deliveries:"
103+
104+
unmatching_mail_jobs.each do |job|
105+
msg << "\n #{mail_job_message(job)}"
106+
end
107+
108+
msg
109+
end
110+
111+
def mail_job_message(job)
112+
mailer_method = job[:args][0..1].join('.')
113+
114+
mailer_args = job[:args][3..-1]
115+
msg_parts = []
116+
msg_parts << "with #{mailer_args}" if mailer_args.any?
117+
msg_parts << "on queue #{job[:queue]}" if job[:queue] && job[:queue] != 'mailers'
118+
msg_parts << "at #{Time.at(job[:at])}" if job[:at]
119+
120+
"#{mailer_method} #{msg_parts.join(', ')}".strip
121+
end
122+
123+
def mailer_job
124+
ActionMailer::DeliveryJob
125+
end
126+
end
127+
# rubocop: enable Style/ClassLength
128+
129+
# @api public
130+
# Passes if an email has been enqueued inside block.
131+
# May chain with to specify expected arguments.
132+
# May chain at_least, at_most or exactly to specify a number of times.
133+
# May chain at to specify a send time.
134+
# May chain on_queue to specify a queue.
135+
#
136+
# @example
137+
# expect {
138+
# MyMailer.welcome(user).deliver_later
139+
# }.to have_enqueued_mail(MyMailer, :welcome)
140+
#
141+
# # Using alias
142+
# expect {
143+
# MyMailer.welcome(user).deliver_later
144+
# }.to enqueue_mail(MyMailer, :welcome)
145+
#
146+
# expect {
147+
# MyMailer.welcome(user).deliver_later
148+
# }.to have_enqueued_mail(MyMailer, :welcome).with(user)
149+
#
150+
# expect {
151+
# MyMailer.welcome(user).deliver_later
152+
# MyMailer.welcome(user).deliver_later
153+
# }.to have_enqueued_mail(MyMailer, :welcome).at_least(:once)
154+
#
155+
# expect {
156+
# MyMailer.welcome(user).deliver_later
157+
# }.to have_enqueued_mail(MyMailer, :welcome).at_most(:twice)
158+
#
159+
# expect {
160+
# MyMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)
161+
# }.to have_enqueued_mail(MyMailer, :welcome).at(Date.tomorrow.noon)
162+
#
163+
# expect {
164+
# MyMailer.welcome(user).deliver_later(queue: :urgent_mail)
165+
# }.to have_enqueued_mail(MyMailer, :welcome).on_queue(:urgent_mail)
166+
def have_enqueued_mail(mailer_class, mail_method_name)
167+
HaveEnqueuedMail.new(mailer_class, mail_method_name)
168+
end
169+
alias_method :have_enqueued_email, :have_enqueued_mail
170+
alias_method :enqueue_mail, :have_enqueued_mail
171+
alias_method :enqueue_email, :have_enqueued_mail
172+
end
173+
end
174+
end

0 commit comments

Comments
 (0)