如何使用rspec测试ActionMailer的deliver_later方法

90

我正在尝试升级到Rails 4.2,并使用delayed_job_active_record。我没有为测试环境设置delayed_job后端,因为我认为这样工作任务将会立即执行。

我正在尝试使用RSpec测试新的“deliver_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))

现在我使用那个会出错。(接收到意外的消息“deliver_later”,但没有参数)

只是

expect(ServiceMailer).to receive(:new_user)

出现问题:'undefined method `deliver_later' for nil:NilClass'

我尝试了一些例子,使用ActiveJob的test_helper来确认任务是否已经入队,但我没有成功测试到正确的任务已经入队。

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

如果包含test_helper,此测试可以通过,但我无法检查发送的是否是正确的电子邮件。

我的目标是:

  • 测试队列中是否有正确的电子邮件(在测试环境中立即执行)
  • 使用正确的参数(@user)

有什么建议吗? 谢谢


14个回答

88

如果我理解正确,您可以这样做:

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)

关键是您需要以某种方式为deliver_later提供一个双倍值。


1
这不应该是 allow(message_delivery).to … 吗?毕竟,您已经通过期望 new_user 来测试结果。 - morgler
1
@morgler 同意。我更新了答案。感谢您的注意和评论。 - Peter Alfvin
1
这可能有点偏题@morgler,但我很好奇你或其他人在一般情况下会怎么想,如果由于某种原因(比如错误),控制器中的deliver_later方法被删除,使用allow我们将无法捕获到,对吗?我的意思是测试仍然会通过。您是否仍然认为使用allow比使用expect更好?我确实看到了expect标志,如果deliver_later被错误地删除,这就是我想要讨论这个问题的基本原因。如果您能更详细地阐述为什么在上下文中使用allow更好,那就太好了。 - boddhisattva
@morgler 感谢您对我的问题的回复。我现在明白了,您主要建议根据测试 ServiceMailernew_user 方法的上下文使用 allow。如果我需要测试 deliver_later,我想我会在现有测试中添加另一个断言(检查 ServiceMailernew_user 方法),以检查类似于 expect(mailer_object).to receive(:deliver_later) 的内容,而不是将其作为另一个完全不同的测试进行测试。如果我们必须测试 deliver_later,很有趣知道您为什么更喜欢单独的测试。 - boddhisattva
@boddhisattva 我更喜欢细粒度的测试,可以立即告诉我出了什么问题。如果我在一个单一的测试中包含了更多的断言,那么我就需要查看测试代码并理解哪个断言失败以及为什么。如果我的测试读作“确保 deliver_later 存在”,我就知道出了什么问题。此外,使用 allow 可以让你有机会允许 deliver_now 和 deliver_later,这样你就可以更改邮件代码的这个方面而不影响这个规范。但是要明确:期望而不是允许也完全可以。 - morgler
显示剩余2条评论

62

使用ActiveJob和rspec-rails 3.4+,你可以像这样使用have_enqueued_job

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

同样要在 config/environments/test.rb 中进行双重检查,确保你已经设置好了:
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test

另一种选择是运行内联作业:

config.active_job.queue_adapter = :inline

但请记住,这将影响您的测试套件的整体性能,因为所有作业都会在入队后立即运行。


8
目前可能需要检查have_enqueued_mail,参考链接:https://relishapp.com/rspec-staging/rspec-rails/docs/matchers/have-enqueued-mail-matcher - new2cpp

38

因为其他答案都不够好,所以我会添加我的答案:

1)没有必要嘲笑 Mailer:Rails 基本上已经为您做了这个。

2)没有必要真正触发电子邮件的创建:这将消耗时间并减慢测试速度!

这就是为什么在 environments/test.rb 中,您应该设置以下选项:

config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test

再次强调:不要使用deliver_now来发送电子邮件,而是始终使用deliver_later。这样可以避免用户等待电子邮件的实际发送。如果没有sidekiqsucker_punch或其他生产工具,只需使用config.active_job.queue_adapter = :async。在开发环境中可以选择asyncinline

针对测试环境,给出以下配置,您的电子邮件将始终被放入队列,而不会执行发送操作:这可以避免您对其进行模拟,并且可以检查它们是否被正确地放入队列。

在测试中,始终将测试分为两个部分: 1)一个单元测试,检查电子邮件是否被正确地放入队列并带有正确的参数。 2)一个单元测试,检查邮件的主题、发件人、收件人和内容是否正确。

给定以下场景:

class User
  after_update :send_email

  def send_email
    ReportMailer.update_mail(id).deliver_later
  end
end

编写一个测试,以检查电子邮件是否已正确排队:

include ActiveJob::TestHelper
expect { user.update(name: 'Hello') }.to have_enqueued_job(ActionMailer::DeliveryJob).with('ReportMailer', 'update_mail', 'deliver_now', user.id)

并为你的电子邮件编写单独的测试

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
  • 您已经确切地测试了您的电子邮件已被排队,而不是一个通用的作业。
  • 您的测试速度很快。
  • 您无需模拟。

当您编写系统测试时,请随意决定是否要在那里真正发送电子邮件,因为速度不再那么重要。我个人喜欢配置以下内容:

RSpec.configure do |config|
  config.around(:each, :mailer) do |example|
    perform_enqueued_jobs do
      example.run
    end
  end
end

并将:mailer属性分配给我想要实际发送电子邮件的测试。

有关如何正确配置Rails中的电子邮件的更多信息,请阅读此文章:https://medium.com/@coorasse/the-correct-emails-configuration-in-rails-c1d8418c0bfd


3
刚刚需要将类从ActionMailer::DeliveryJob更改为ActionMailer::MailDeliveryJob - haffla
这是一个很棒的答案! - Holger Frohloff
感谢!在Rails 6中,我只需要将have_enqueued_job(ActionMailer::DeliveryJob)更改为on_queue('mailers'),这样就变成了expect { user.update(name: 'Hello') }.to have_enqueued_job.on_queue('mailers').with('ReportMailer', 'update_mail', 'deliver_now', user.id) - Pedro Schmitt
2
作为这种方法的变体,您可以使用have_enqueued_mail匹配器进行检查,参见 https://relishapp.com/rspec/rspec-rails/v/5-0/docs/matchers/have-enqueued-mail-matcher - Benjamin

38

如果您发现这个问题,但是使用的是ActiveJob而不仅仅是DelayedJob本身,并且正在使用Rails 5,则建议在config/environments/test.rb中配置ActionMailer:

config.active_job.queue_adapter = :inline

(这是在Rails 5之前的默认行为)


在运行规格时,它不会执行所有异步任务吗? - Aleksey
没错,这是一个很好的观点。在ActiveJob的简单和轻量级应用场景中,您可以将所有异步任务配置为内联运行,它会非常有用,并且使测试变得简单。 - Gabe Kopley
1
可能刚刚节省了我一个小时的调试时间。谢谢! - Matt
这个之前运作得很好,但最近的打包更新似乎导致它停止了:( - 有任何想法吗? - Hackeron

12

5
这对我有用,但是我当时使用了deliver_later(wait: 2.minutes)。所以我改成了deliver_later(options={}) - rigelstpierre
8
应用程序可以发送同步和异步电子邮件,这是一种技巧,可以使其在测试中难以区分。 - Jeriko
2
我同意黑客是个坏主意。将_later别名为_now只会带来痛苦。 - John Paul Ashenfelter
2
那个链接已经失效了,但我在 Wayback Machine 上找到了它;http://web.archive.org/web/20150710184659/http://www.mrlab.sk/testing-email-delivery-with-deliver-later.html - OzBarry
我收到了 NameError: uninitialized constant ActionMailer 的错误。 - Albert Català
请记得在相关规范或spec_helper/rails_helper中包含帮助程序,除非您已设置为自动加载。这在2020年2月的rails 6 rspec最新版本中对我有效。我没有测试任何同步与异步电子邮件,只是想要一个快速修复而不需要太多麻烦。 - Nick M

12

一个比更改deliver_later更好的解决方案是:

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

around { |example| perform_enqueued_jobs(&example) } 确保在检查测试值之前运行后台任务。


这种方法更易于理解,但如果被测试的操作排队执行任何耗时的作业,则会显著减慢您的测试速度。 - niborg
这也不测试选择哪个邮件程序/操作。如果您的代码涉及有条件地选择不同的邮件,则无法帮助。 - Cyril Duchon-Doris

6

我曾经有同样的疑问,并且受到这个答案的启发,用更简洁(一行)的方式解决了问题。

expect(ServiceMailer).to receive_message_chain(:new_user, :deliver_later).with(@user).with(no_args)

请注意,最后的with(no_args)是必需的。
但是,如果您不介意是否调用deliver_later,只需执行以下操作: expect(ServiceMailer).to expect(:new_user).with(@user).and_call_original

4
一种简单的方法是:
expect(ServiceMailer).to(
  receive(:new_user).with(@user).and_call_original
)
# subject

3

最近加入谷歌公司的员工:

allow(YourMailer).to receive(:mailer_method).and_call_original

expect(YourMailer).to have_received(:mailer_method)

2

该回答适用于Rails测试,而非rspec...

如果您使用delivery_later,请按以下方式操作:

# app/controllers/users_controller.rb 

class UsersController < ApplicationControllerdef create# Yes, Ruby 2.0+ keyword arguments are preferred 
    UserMailer.welcome_email(user: @user).deliver_later 
  end 
end 

您可以在测试中检查电子邮件是否已添加到队列中:
# 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 

如果你这么做,你会惊讶地发现测试失败,告诉你我们无法使用assert_enqueued_jobs。

这是因为我们的测试继承自ActionController::TestCase,而在撰写本文时,它不包括ActiveJob::TestHelper。

但是我们可以很快地解决这个问题:

# test/test_helper.rb 

class ActionController::TestCase 
  include ActiveJob::TestHelperend 

Reference: https://www.engineyard.com/blog/testing-async-emails-rails-42


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接