How to test ActionMailer deliver_later with rspec
Ruby on-RailsRspecActionmailerDelayed JobRails ActivejobRuby on-Rails Problem Overview
trying to upgrade to Rails 4.2, using delayed_job_active_record. I've not set the delayed_job backend for test environment as thought that way jobs would execute straight away.
I'm trying to test the new 'deliver_later' method with RSpec, but I'm not sure how.
Old controller code:
ServiceMailer.delay.new_user(@user)
New controller code:
ServiceMailer.new_user(@user).deliver_later
I USED to test it like so:
expect(ServiceMailer).to receive(:new_user).with(@user).and_return(double("mailer", :deliver => true))
Now I get errors using that. (Double "mailer" received unexpected message :deliver_later with (no args))
Just
expect(ServiceMailer).to receive(:new_user)
fails too with 'undefined method `deliver_later' for nil:NilClass'
I've tried some examples that allow you to see if jobs are enqueued using test_helper in ActiveJob but I haven't managed to test that the correct job is queued.
expect(enqueued_jobs.size).to eq(1)
This passes if the test_helper is included, but it doesn't allow me to check it is the correct email that is being sent.
What I want to do is:
- test that the correct email is queued (or executed straight away in test env)
- with the correct parameters (@user)
Any ideas?? thanks
Ruby on-Rails Solutions
Solution 1 - Ruby on-Rails
If I understand you correctly, you could do:
message_delivery = instance_double(ActionMailer::MessageDelivery)
expect(ServiceMailer).to receive(:new_user).with(@user).and_return(message_delivery)
allow(message_delivery).to receive(:deliver_later)
The key thing is that you need to somehow provide a double for deliver_later
.
Solution 2 - Ruby on-Rails
Using ActiveJob and rspec-rails
3.4+, you could use have_enqueued_job
like this:
expect {
YourMailer.your_method.deliver_later
# or any other method that eventually would trigger mail enqueuing
}.to(
have_enqueued_job.on_queue('mailers').with(
# `with` isn't mandatory, but it will help if you want to make sure is
# the correct enqueued mail.
'YourMailer', 'your_method', 'deliver_now', any_param_you_want_to_check
)
)
also double check in config/environments/test.rb
you have:
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
Another option would be to run inline jobs:
config.active_job.queue_adapter = :inline
But keep in mind this would affect the overall performance of your test suite, as all your jobs will run as soon as they're enqueued.
Solution 3 - Ruby on-Rails
If you find this question but are using ActiveJob rather than simply DelayedJob on its own, and are using Rails 5, I recommend configuring ActionMailer in config/environments/test.rb
:
config.active_job.queue_adapter = :inline
(this was the default behavior prior to Rails 5)
Solution 4 - Ruby on-Rails
I will add my answer because none of the others was good enough for me:
-
There is no need to mock the Mailer: Rails basically does that already for you.
-
There is no need to really trigger the creation of the email: this will consume time and slow down your test!
That's why in environments/test.rb
you should have the following options set:
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
Again: don't deliver your emails using deliver_now
but always use deliver_later
. That prevents your users from waiting for the effective delivering of the email. If you don't have sidekiq
, sucker_punch
, or any other in production, simply use config.active_job.queue_adapter = :async
. And either async
or inline
for development environment.
Given the following configuration for the testing environment, you emails will always be enqueued and never executed for delivery: this prevents your from mocking them and you can check that they are enqueued correctly.
In you tests, always split the test in two:
- One unit test to check that the email is enqueued correctly and with the correct parameters
- One unit test for the mail to check that the subject, sender, receiver and content are correct.
Given the following scenario:
class User
after_update :send_email
def send_email
ReportMailer.update_mail(id).deliver_later
end
end
Write a test to check the email is enqueued correctly:
include ActiveJob::TestHelper
expect { user.update(name: 'Hello') }.to have_enqueued_job(ActionMailer::DeliveryJob).with('ReportMailer', 'update_mail', 'deliver_now', user.id)
and write a separate test for your email
Rspec.describe ReportMailer do
describe '#update_email' do
subject(:mailer) { described_class.update_email(user.id) }
it { expect(mailer.subject).to eq 'whatever' }
...
end
end
- You have tested exactly that your email has been enqueued and not a generic job.
- Your test is fast
- You needed no mocking
When you write a system test, feel free to decide if you want to really deliver emails there, since speed doesn't matter that much anymore. I personally like to configure the following:
RSpec.configure do |config|
config.around(:each, :mailer) do |example|
perform_enqueued_jobs do
example.run
end
end
end
and assign the :mailer
attribute to the tests were I want to actually send emails.
For more about how to correctly configure your email in Rails read this article: https://medium.com/@coorasse/the-correct-emails-configuration-in-rails-c1d8418c0bfd
Solution 5 - Ruby on-Rails
Add this:
# spec/support/message_delivery.rb
class ActionMailer::MessageDelivery
def deliver_later
deliver_now
end
end
Reference: http://mrlab.sk/testing-email-delivery-with-deliver-later.html
Solution 6 - Ruby on-Rails
A nicer solution (than monkeypatching deliver_later
) is:
require 'spec_helper'
include ActiveJob::TestHelper
describe YourObject do
around { |example| perform_enqueued_jobs(&example) }
it "sends an email" do
expect { something_that.sends_an_email }.to change(ActionMailer::Base.deliveries, :length)
end
end
The around { |example| perform_enqueued_jobs(&example) }
ensures that background tasks are run before checking the test values.
Solution 7 - Ruby on-Rails
I came with the same doubt and resolved in a less verbose (single line) way inspired by this answer
expect(ServiceMailer).to receive_message_chain(:new_user, :deliver_later).with(@user).with(no_args)
Note that the last with(no_args)
is essential.
But, if you don't bother if deliver_later
is being called, just do:
expect(ServiceMailer).to expect(:new_user).with(@user).and_call_original
Solution 8 - Ruby on-Rails
A simple way is:
expect(ServiceMailer).to(
receive(:new_user).with(@user).and_call_original
)
# subject
Solution 9 - Ruby on-Rails
This answer is for Rails Test, not for rspec...
If you are using delivery_later
like this:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
…
def create
…
# Yes, Ruby 2.0+ keyword arguments are preferred
UserMailer.welcome_email(user: @user).deliver_later
end
end
You can check in your test if the email has been added to the queue:
# test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
…
test 'email is enqueued to be delivered later' do
assert_enqueued_jobs 1 do
post :create, {…}
end
end
end
If you do this though, you’ll surprised by the failing test that tells you assert_enqueued_jobs is not defined for us to use.
This is because our test inherits from ActionController::TestCase which, at the time of writing, does not include ActiveJob::TestHelper.
But we can quickly fix this:
# test/test_helper.rb
class ActionController::TestCase
include ActiveJob::TestHelper
…
end
Reference: https://www.engineyard.com/blog/testing-async-emails-rails-42
Solution 10 - Ruby on-Rails
For recent Googlers:
allow(YourMailer).to receive(:mailer_method).and_call_original
expect(YourMailer).to have_received(:mailer_method)
Solution 11 - Ruby on-Rails
I think one of the better ways to test this is to check the status of job alongside the basic response json checks like:
expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.on_queue('mailers').with('mailer_name', 'mailer_method', 'delivery_now', { :params => {}, :args=>[] } )
Solution 12 - Ruby on-Rails
I have come here looking for an answer for a complete testing, so, not just asking if there is one mail waiting to be sent, in addition, for its recipient, subject...etc
I have a solution, than comes from here, but with a little change:
As it says, the curial part is
mail = perform_enqueued_jobs { ActionMailer::DeliveryJob.perform_now(*enqueued_jobs.first[:args]) }
The problem is that the parameters than mailer receives, in this case, is different from the parameters than receives in production, in production, if the first parameter is a Model, now in testing will receive a hash, so will crash
enqueued_jobs.first[:args]
["UserMailer", "welcome_email", "deliver_now", {"_aj_globalid"=>"gid://forjartistica/User/1"}]
So, if we call the mailer as UserMailer.welcome_email(@user).deliver_later
the mailer receives in production a User, but in testing will receive {"_aj_globalid"=>"gid://forjartistica/User/1"}
All comments will be appreciate, The less painful solution I have found is changing the way that I call the mailers, passing, the model's id and not the model:
UserMailer.welcome_email(@user.id).deliver_later
Solution 13 - Ruby on-Rails
This answer is a little bit different, but may help in cases like a new change in the rails API, or a change in the way you want to deliver (like use deliver_now
instead of deliver_later
).
What I do most of the time is to pass a mailer as a dependency to the method that I am testing, but I don't pass an mailer from rails, I instead pass an object that will do the the things in the "way that I want"...
For example if I want to check that I am sending the right mail after the registration of a user... I could do...
class DummyMailer
def self.send_welcome_message(user)
end
end
it "sends a welcome email" do
allow(store).to receive(:create).and_return(user)
expect(mailer).to receive(:send_welcome_message).with(user)
register_user(params, store, mailer)
end
And then in the controller where I will be calling that method, I would write the "real" implementation of that mailer...
class RegistrationsController < ApplicationController
def create
Registrations.register_user(params[:user], User, Mailer)
# ...
end
class Mailer
def self.send_welcome_message(user)
ServiceMailer.new_user(user).deliver_later
end
end
end
In this way I feel that I am testing that I am sending the right message, to the right object, with the right data (arguments). And I am just in need of creating a very simple object that has no logic, just the responsibility of knowing how ActionMailer wants to be called.
I prefer to do this because I prefer to have more control over the dependencies I have. This is form me an example of the "Dependency inversion principle".
I am not sure if it is your taste, but is another way to solve the problem =).