Delayed_Job - handle_asynchronously with ActionMailer? - ruby-on-rails

So I'm using Delayed Jobs and I'm trying to figure out how to get all of my mailers to be delayed. Right now, I've put handle_asynchronously on all of my action mailer methods… but I don't think that is going to work.
def first_notification(time)
#time = time
mail :to => time.person.email,
:from => "email#example.com",
:subject => "#{time.person.name} wants to say hi"
end
handle_asynchronously :advisor_first_notification, :priority => 20
The reason I don't think this is going to work is because I call it as such:
UserMailer.first_notification(#time).deliver
So how would it handle the .deliver part of this? Right now I get an exception.
EXCEPTION: #<ArgumentError: wrong number of arguments (1 for 0)>
Which makes me feel that something is getting messed up in the deliver aspect.
I would rather not have a separate job file for each email (as I have a lot of them), so what is the proper way to handle this?
The only other option I can think of is to encapsulate the calls into a method within my models and have them have the handle_asynchronously - that way they can call the entire thing at once.

The mailer is a bit tricky... Instead of using the handle_asynchronously syntax:
UserMailer.delay.first_notification(#time)
The 'trick' is having delay() before the mailer method

Further to Jesse's answer, the collectiveidea's fork of delayed_job indicates that you should definitely not use the deliver method at all with Rails 3 Mailer code:
# without delayed_job
Notifier.signup(#user).deliver
# with delayed_job
Notifier.delay.signup(#user)

I've gotten it to work by doing the following:
class MyMailer < ActionMailer::Base
def send_my_mail_method(*args)
MyMailer.my_mail_method(*args).deliver
end
handle_asynchronously :send_my_mail_method
def my_mail_method(*args)
# mail call ...
end
end
I like this way because it allows me to test that delivery happens interactively, without having to do something stupid like mock the delay call.

Related

How to test ActionMailer deliver_later with rspec

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 =).

Delayed job; does it skip before filter

I have a delayed job that runs perfect against a public schema in postgresql.
Most of my operations however are against other schemas (one for each client)
To handle different schemas I've followed the instructions and put code to switch search path, in my before_filter (in application controller).
I've noticed. That the code in the before_filter gets called perfectly during typical operations, but not at all during delayed job.
I trimmed and trimmed out everything but the simplest thing I could think of, to show entrance.
class ApplicationController < ActionController::Base
protect_from_forgery
def write_to_log(text)
File.open('c:\temp.txt', 'ab') do |f|
f.write text + "\r\n"
f.close
end
end
before_filter :on_before_filter
def on_before_filter
write_to_log('hey dave');
return if(use_token() == false);
set_active_schema if(goto_log_in? == false);
end
The code in the worker class
def run_job(id)
upload = Upload.find(id)
upload.run_job();
end
handle_asynchronously :run_job, :priority => 10, :queue => 'public'
Quite standard stuff? Though the code in the job runs, the before_filter code doesn't get called.
So my question is. Did I do something wrong? Or more importantly, how can I do something right?
I'm not recommending this approach; I'm just answering your question by providing this code. Since you essentially want your code to run before any attempted call to the database, you can monkey patch ActiveRecord. Add the following code to config/initializers/active_record_monkey_patch.rb
class ActiveRecord::ConnectionAdapters::ConnectionPool
# create an alias for the old 'connection' method
alias_method :old_connection, :connection
# redefine the 'connection' method
def connection
# output something just to make sure the monkey patch is working
puts "*** custom connection method called ***"
# your custom code is here
write_to_log('hey dave');
return if(use_token() == false);
set_active_schema if(goto_log_in? == false);
# call the old 'connection' method
old_connection
end
end
You'll see your custom connection method getting called frequently now, and it will work without a controller. You can test it by opening up a rails console and performing any database query, and you should see the "custom connection method called" message displayed several times.
If you want to manipulate the ActiveRecord search path for Postgres and schemas you can use a full-featured gem like apartment: https://github.com/bradrobertson/apartment
You can switch to a new schema:
Apartment::Database.switch('database_name')
Regardless if you call this in an application controller request or a background job.

Delayed_job not sending Rails 3 emails

Note: Using Rails 3.1 and current delayed_job gem.
I have a User model that calls after_create :mail_confirmation.
The mail_confirmation method looks like the following, per the delayed_job instructions:
def mail_confirmation
UserMailer.delay.registration_confirmation(self)
end
The UserMailer is:
class UserMailer < ActionMailer::Base
default from: "test#mysite.com"
def registration_confirmation(user)
#user = user
mail(:to => "#{user.full_name} <#{user.email}>", :subject => "Test registration email")
end
end
The job is queued, and the output from rake jobs:work makes it seem as if it completed successfully:
[Worker(host:mymac.local pid:73694)] Starting job worker
[Worker(host:mymac.local pid:73694)] Class#registration_confirmation completed after 1.3659
[Worker(host:mymac.local pid:73694)] 1 jobs processed at 0.7288 j/s, 0 failed ...
There is no error but the email is never sent. It works fine if I remove delayed from the method call in the User model and go with the standard deliver method:
def mail_confirmation
UserMailer.registration_confirmation(self).deliver
end
How can I find out what is happening when the job is processed? Any idea how to fix it?
Update It appears that it is related to this:
NoMethodError with delayed_job (collectiveidea gem)
Yeah i had this same issue. #Clay is correct, there is an issue at the moment: https://github.com/collectiveidea/delayed_job/issues/323
I resolved this problem by reverting back to the previous version of delayed_job.
gem 'delayed_job', '2.1.2'
I'm having the same issues here. I discovered that for some reason the delay method called on Mailer classes is being handled by the method Delayed::MessageSending#delay instead of Delayed::DelayMail#delay which instantiates the right performable (which is PerformableMailer instead of PerformableMethod). It doesn't crash the job because PerformableMethod just calls the method without the deliver.
Take a look at:
delayted_job/lib/delayed/message_sending.rb
delayted_job/lib/delayed/performable_mailer.rb

How to stop from view being rendered while sending email

I am using Rails 3.0.9 and I have following code to send an email when a comment is posted.
class Mailer < ActionMailer::Base
def comment_notification(comment)
User.active.each do |user|
#user = user
mail(:to => #user.email, :subject => subject)
end
end
end
If there are not active users then User.active is empty and the code inside does not get executed. However the view is rendered and view fails because #user is missing.
The above code is invoked by observer
Mailer.comment_notification(comment).deliver
One way to fix this problem would be to change the code in observer to something like this
User.active.each do |recipient|
Mailer.comment_notification(comment, recipient).deliver
end
Is this the right way to fix this way. I would like my observer to be as thin as possible.
Yes, your observer fix is correct. You should loop through and send emails one by one. The mailer should just send one email at a time. This is a job best left to Delayed Job though. You don't want to waiting around while an email sends.
Here is a tutorial on Delayed Job: http://railscasts.com/episodes/171-delayed-job
Be sure to check the Readme for Delayed Job as well, paying special attention to the "Rails 3 Mailers" section: http://github.com/collectiveidea/delayed_job

How do you return out of an ActionMailer::Base function?

I'm trying to implement an ActionMailer function that will send out a newsletter to a specific user. I want to make sure that the newsletter is only sent to subscribed users. I tried implementing it like so:
class UserMailer < ActionMailer::Base
def newsletter(user)
return unless user.subscribed # This still renders my mailer view
mail(:to => user.email, :subject => "Newsletter")
end
end
The problem is that the return unless user.subscribed line still appears to be rendering the mailer view and is still sent by the calling code (from a cron job):
task :cron => :environment do
User.where(:subscribed => true).each do |user|
UserMailer.newsletter(user).deliver
end
end
Note that I do have that subscription logic in my cron job as well for performance reasons (shouldn't have to iterate over ALL users, only those that are subscribed). However, it feels like the UserMailer class is the right place for this logic to exist (otherwise any other location that calls the newsletter method will need to check the subscribed flag as well.
The Mailer, IMHO, is the wrong place for this logic. The mailer should do nothing but format and send messages. The logic to decide whether or not to send should be within the calling block of code. It's not the right way, but something as simple as:
UserMailer.newsletter(user).deliver if user.subscribed?
Alternately, as you mentioned, you shouldn't have to iterate over all users, just the subscribed. So with a scope in the User model called subscribed:
User.subscribed.each do |user|
UserMailer.newsletter(user).deliver
end
This way you don't need to test on a per-user basis; only the subscribed users are included, and the logic is in the calling block, not in the mailer.

Resources