Skip to content

Feature/have enqueued mail matcher #2047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jan 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5de5efa
Write first test case for calling simple mailer method with deliver_l…
Dec 11, 2018
7e7a218
Move deliver_later gem logic into HaveEnqueuedMail matcher
Dec 11, 2018
1692d1d
Add tests for arguments matcher and error messages. Largely based on…
Dec 17, 2018
003b3fa
Add exactly, at_least, at_most functionality for have_enqueued_mail m…
Dec 17, 2018
0af1edd
Support mailer methods with default/optional arguments; Support match…
Dec 17, 2018
cd61a4d
Correct private YARD doc on RSpec::Rails::Matchers::HaveEnqueuedJob
Dec 17, 2018
63e9caf
Provide unmatching enqueued email info in failure messages
Dec 24, 2018
cd7ad09
Add tests for have_enqueued_mail matcher at_least and at_most modifiers
Dec 24, 2018
765a9fe
Add support for `at` and `on_queue`
Dec 24, 2018
0f6eb1a
Explicitly require rspec/mocks for access to `anything` method
Dec 24, 2018
e2df63a
Ignore Rubocop class length for HaveEnqueuedMail class
Dec 24, 2018
8a29d96
Replace .negative? with < 0 for older ruby versions
Dec 24, 2018
5e9263f
Remove .inspect call from test to see if that works...
Dec 24, 2018
fdff42b
Remove %i array syntax for older ruby versions
Dec 24, 2018
9f6e792
Remove the send_time part of the error message to see if that is caus…
Dec 24, 2018
1929b11
Replace new Hash key syntax with old => format for Ruby 1.8.7
Dec 24, 2018
3b507fb
Call strftime in test to get some semblance of timestamp verification
Dec 24, 2018
f588547
Fix Ruby 1.8.7 syntax errors
Dec 25, 2018
0d1b0c9
Fix ActiveJob::Base.queue_adapter error
Dec 31, 2018
b08f1c7
Refactor HaveEnqueuedMail matcher to be a subclass of the HaveEnqueue…
Dec 31, 2018
5753c6c
Support with blocks
Jan 3, 2019
b32449e
Test that non-mailer jobs do not appear in the have_enqueued_mail mat…
Jan 3, 2019
c5b5895
Make 'deliver_now' a frozen constant
Jan 4, 2019
c8cc534
Only pass a mailer-specific block to super if a block is provided
Jan 4, 2019
53ac74a
Fix hash rocket syntax for Ruby 1.8.7
Jan 11, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions lib/rspec/rails/matchers/have_enqueued_mail.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
require "rspec/mocks"
require "rspec/rails/matchers/active_job"

module RSpec
module Rails
module Matchers
# Matcher class for `have_enqueued_mail`. Should not be instantiated directly.
#
# rubocop: disable Style/ClassLength
# @private
# @see RSpec::Rails::Matchers#have_enqueued_mail
class HaveEnqueuedMail < ActiveJob::HaveEnqueuedJob
MAILER_JOB_METHOD = 'deliver_now'.freeze

include RSpec::Mocks::ExampleMethods

def initialize(mailer_class, method_name)
super(mailer_job)
@mailer_class = mailer_class
@method_name = method_name
@mail_args = []
@args = mailer_args
end

def description
"enqueues #{@mailer_class.name}.#{@method_name}"
end

def with(*args, &block)
@mail_args = args
block.nil? ? super(*mailer_args) : super(*mailer_args, &yield_mail_args(block))
end

def matches?(block)
raise ArgumentError, 'have_enqueued_mail and enqueue_mail only work with block arguments' unless block.respond_to?(:call)
check_active_job_adapter
super
end

def failure_message
"expected to enqueue #{base_message}".tap do |msg|
msg << "\n#{unmatching_mail_jobs_message}" if unmatching_mail_jobs.any?
end
end

def failure_message_when_negated
"expected not to enqueue #{base_message}"
end

private

def base_message
"#{@mailer_class.name}.#{@method_name}".tap do |msg|
msg << " #{expected_count_message}"
msg << " with #{@mail_args}," if @mail_args.any?
msg << " on queue #{@queue}," if @queue
msg << " at #{@at.inspect}," if @at
msg << " but enqueued #{@matching_jobs.size}"
end
end

def expected_count_message
"#{message_expectation_modifier} #{@expected_number} #{@expected_number == 1 ? 'time' : 'times'}"
end

def mailer_args
if @mail_args.any?
base_mailer_args + @mail_args
else
mailer_method_arity = @mailer_class.instance_method(@method_name).arity

number_of_args = if mailer_method_arity < 0
(mailer_method_arity + 1).abs
else
mailer_method_arity
end

base_mailer_args + Array.new(number_of_args) { anything }
end
end

def base_mailer_args
[@mailer_class.name, @method_name.to_s, MAILER_JOB_METHOD]
end

def yield_mail_args(block)
Proc.new { |*job_args| block.call(*(job_args - base_mailer_args)) }
end

def check_active_job_adapter
return if ::ActiveJob::QueueAdapters::TestAdapter === ::ActiveJob::Base.queue_adapter
raise StandardError, "To use HaveEnqueuedMail matcher set `ActiveJob::Base.queue_adapter = :test`"
end

def unmatching_mail_jobs
@unmatching_jobs.select do |job|
job[:job] == mailer_job
end
end

def unmatching_mail_jobs_message
msg = "Queued deliveries:"

unmatching_mail_jobs.each do |job|
msg << "\n #{mail_job_message(job)}"
end

msg
end

def mail_job_message(job)
mailer_method = job[:args][0..1].join('.')

mailer_args = job[:args][3..-1]
msg_parts = []
msg_parts << "with #{mailer_args}" if mailer_args.any?
msg_parts << "on queue #{job[:queue]}" if job[:queue] && job[:queue] != 'mailers'
msg_parts << "at #{Time.at(job[:at])}" if job[:at]

"#{mailer_method} #{msg_parts.join(', ')}".strip
end

def mailer_job
ActionMailer::DeliveryJob
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, but why not just use the constant name directly in places?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the hardest time keeping the constant straight in my head. I kept wanting to call it ActionDelivery::MailerJob or ActionMailer::DeliverJob. So I guess out of frustration, I just made it a method 😅

end
# rubocop: enable Style/ClassLength

# @api public
# Passes if an email has been enqueued inside block.
# May chain with to specify expected arguments.
# May chain at_least, at_most or exactly to specify a number of times.
# May chain at to specify a send time.
# May chain on_queue to specify a queue.
#
# @example
# expect {
# MyMailer.welcome(user).deliver_later
# }.to have_enqueued_mail(MyMailer, :welcome)
#
# # Using alias
# expect {
# MyMailer.welcome(user).deliver_later
# }.to enqueue_mail(MyMailer, :welcome)
#
# expect {
# MyMailer.welcome(user).deliver_later
# }.to have_enqueued_mail(MyMailer, :welcome).with(user)
#
# expect {
# MyMailer.welcome(user).deliver_later
# MyMailer.welcome(user).deliver_later
# }.to have_enqueued_mail(MyMailer, :welcome).at_least(:once)
#
# expect {
# MyMailer.welcome(user).deliver_later
# }.to have_enqueued_mail(MyMailer, :welcome).at_most(:twice)
#
# expect {
# MyMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)
# }.to have_enqueued_mail(MyMailer, :welcome).at(Date.tomorrow.noon)
#
# expect {
# MyMailer.welcome(user).deliver_later(queue: :urgent_mail)
# }.to have_enqueued_mail(MyMailer, :welcome).on_queue(:urgent_mail)
def have_enqueued_mail(mailer_class, mail_method_name)
HaveEnqueuedMail.new(mailer_class, mail_method_name)
end
alias_method :have_enqueued_email, :have_enqueued_mail
alias_method :enqueue_mail, :have_enqueued_mail
alias_method :enqueue_email, :have_enqueued_mail
end
end
end
Loading