如何使用 rspec 测试 ActionMailer 传递_later

尝试使用  延時 _ job _ active _ record 升級到 Rails4.2。我没有为测试环境设置 late _ job 后端,因为我认为这样就可以立即执行作业。

我正在尝试用 RSpec 测试新的“ give _ later”方法,但我不确定如何测试。

旧控制器代码:

ServiceMailer.delay.new_user(@user)

新的控制器代码:

ServiceMailer.new_user(@user).deliver_later

我以前是这样测试的:

expect(ServiceMailer).to receive(:new_user).with(@user).and_return(double("mailer", :deliver => true))

现在我使用它得到了错误。(双“ mailer”收到了意外的消息: give _ later with (no args))

只是

expect(ServiceMailer).to receive(:new_user)

对于 nil: NilClass,’未定义的方法‘ give _ later’也失败了

我已经尝试了一些示例,这些示例允许您使用 ActiveJob 中的 test _ helper 查看作业是否排队,但是我还没有设法测试正确的作业是否排队。

expect(enqueued_jobs.size).to eq(1)

如果包含 test _ helper,这将通过,但是它不允许我检查发送的电子邮件是否正确。

我想做的是:

  • 测试正确的电子邮件是否排队(或者直接在 test env 中执行)
  • 使用正确的参数(@user)

有什么想法吗? 谢谢

39706 次浏览

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.

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.

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.

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

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 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

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

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

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=>[] } )