I have a Cucumber test that drives out someone completing a form, the output of which should be two emails (one to the user, one to an admin).
Setup
Following this guide I have set up a wrapper around any scenario marked #email.
Around('#email') do |_scenario, block|
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries.clear
block.call
end
ActionMailer::Base.delivery_method = :test was already set in my config/environments/test.rb file.
The scenario looks something like this (edited to remove business wording)
#email
Scenario: Can find out more
Given I am a visitor
When I visit a page
And I complete the form
Then I should get an email, as should the admin
The related definitions are;
When(/^I complete the form$/) do
fill_in('Email Address', with: 'test#test.com')
fill_in('Name', with: 'Test Name')
click_button('Subscribe')
end
Then(/^I should get an email, as should the admin$/) do
expect(ActionMailer::Base.deliveries.size).to eq 2
end
The Code
This code works when doing a dry run locally, just not under test.
def subscribe
SubscribeMailer.user_subscribe(params[:email]).deliver_later
SubscribeMailer.subscribe_email(params[:name],
params[:email],
request.referrer).deliver_later
redirect_to subscribe_thanks_path
end
The user_subscribe and subscribe_email just make some instance variables and call mail.
Throwing some debuggers around I can see all parts of the system I would expect to get hit appear to do so.
Expected result
I would expect this to always send two emails (or zero emails in the case of me doing something wrong).
Actual result
What I get back is consistently 1 email.
The email I get back is the user_subscribe one.
I found the solution.
I'm using Rails 5 and between 4 and 5 the default behaviour of config.active_job.queue_adapter changed.
Adding config.active_job.queue_adapter = :inline into config/environments/test.rb fixed this issue. It now accurately counts the emails sent.
class MyMailer < BaseMailer
def send_login_prompt
User.reset_login
# ...
end
end
describe MyMailer do
context "when sending a login prompt" do
it "should reset the user's password" do
expect(User).to receive(:reset_login)
MyMailer.send_login_prompt
end
end
end
The incarnation of the test above fails
In the example above we've got a mailer method that triggers a user's password to be
reset. We want to test for that. However when you run the test, .send_login_prompt
does not get called.
In order to actually trigger the code to be evaluated you have to add a line to
inspect the mail object:
Test that works
it "should reset the user's password" do
expect(User).to receive(:reset_login)
mail = MyMailer.send_login_prompt
mail.body # after this line is added the mail is built and the test passes
end
Why is the method not called before and what's the most correct way to prompt the
method to be evaluated rather than the ad-hoc route of calling mail.body?
The mail is not delivered until you access it's content or you can one of the .deliver functions.
This should get the job done nicely
mail = MyMailer.send_login_prompt.deliver_now
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
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.
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.
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)
I will add my answer because none of the others was good enough for me:
1) There is no need to mock the Mailer: Rails basically does that already for you.
2) 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:
1) One unit test to check that the email is enqueued correctly and with the correct parameters
2) 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
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
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.
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
A simple way is:
expect(ServiceMailer).to(
receive(:new_user).with(#user).and_call_original
)
# subject
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
For recent Googlers:
allow(YourMailer).to receive(:mailer_method).and_call_original
expect(YourMailer).to have_received(:mailer_method)
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=>[] } )
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
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 =).
The rails guides example code for testing mailers is as follows:
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
tests UserMailer
test "invite" do
# Send the email, then test that it got queued
email = UserMailer.create_invite('me#example.com',
'friend#example.com', Time.now).deliver
assert !ActionMailer::Base.deliveries.empty?
# Test the body of the sent email contains what we expect it to
assert_equal ['me#example.com'], email.from
assert_equal ['friend#example.com'], email.to
assert_equal 'You have been invited by me#example.com', email.subject
assert_equal read_fixture('invite').join, email.body.to_s
end
end
What is the purpose of this line and do I have to add it in my tests? -->
tests UserMailer
Please check the tests method implementation
Here is what carlosantoniodasilva says about tests
You can use tests in mailers/controllers/helpers tests to define
which mailer/controller/helper you're testing, in case it cannot be
automatically guessed from the test name. eg, UserMailerTest
automatically guesses UserMailer as the test subject, but you can
explicitly say that or say it's a different name with the tests
method. So having the line there is not entirely wrong, it's just
being explicit about what Rails does automatically.
You can read the entire discussion on this very specific topic at Github
I am using rspec-rails and I want to test that my mailer is rendering the correct view template.
describe MyMailer do
describe '#notify_customer' do
it 'sends a notification' do
# fire
email = MyMailer.notify_customer.deliver
expect(ActionMailer::Base.deliveries).not_to be_empty
expect(email.from).to include "cs#mycompany.com"
# I would like to test here something like
# ***** HOW ? *****
expect(template_path).to eq("mailers/my_mailer/notify_customer")
end
end
end
Is this a valid approach? Or shall I do something completely different to that?
Update
MyMailer#notify_customer might have some logic (e.g. depending on the locale of the customer) to choose different template under different circumstances. It is more or less similar problem with controllers rendering different view templates under different circumstances. With RSpec you can write
expect(response).to render_template "....."
and it works. I am looking for something similar for the mailers.
I think this is a step closer to the answer above, since it does test for implicit templates.
# IMPORTANT!
# must copy https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/support/helpers/next_instance_of.rb
it 'renders foo_mail' do
allow_next_instance_of(described_class) do |mailer|
allow(mailer).to receive(:render_to_body).and_wrap_original do |m, options|
expect(options[:template]).to eq('foo_mail')
m.call(options)
end
end
body = subject.body.encoded
end
OK, I understand what you're trying to achieve now.
You should be able to test which template is called by setting expectations on your mailer for the mail method having been called with particular arguments.
Try this in your test:
MyMailer.should_receive(:mail).with(hash_including(:template => 'expected_template'))