如何使用rspec测试ActionMailerliver_later


75

尝试使用delay_job_active_record升级到Rails 4.2。我没有为测试环境设置delay_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))

现在,我在使用它时遇到了错误。(双“ mailer”收到意外消息:带有(无参数)的deliver_later)

只是

expect(ServiceMailer).to receive(:new_user)

对于nil:NilClass,也使用“未定义的方法'deliver_later'”失败

我尝试了一些示例,这些示例使您可以查看是否使用ActiveJob中的test_helper将作业排入了队列,但我还没有设法测试排队的正确作业。

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

如果包括test_helper,则通过,但不允许我检查它是否发送了正确的电子邮件。

我想做的是:

  • 测试正确的电子邮件已排队(或在测试环境中立即执行)
  • 具有正确的参数(@user)

有任何想法吗??谢谢


Answers:


80

如果我对您的理解正确,则可以执行以下操作:

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


这不是allow(message_delivery).to …吗?毕竟,您已经通过期望检验了结果new_user
morgler '16

1
@morgler同意。我更新了答案。感谢您的关注/评论。
彼得·阿尔夫文,2016年

1
这可能与主题@morgler有点偏离,但是我很想知道您或其他人的总体看法(如果出于某种原因(例如由于错误))说,该deliver_later方法已从控制器中删除,通过使用allow我们将无法抓住那个权利?我的意思是测试仍会通过。您是否仍然认为使用allow会比使用更好的主意expect?我确实看到该expect标志是否deliver_later被错误地删除了,这基本上就是为什么我想对此进行一般讨论。如果您能详细说明allow在上述情况下为什么更好,那将是很好的。
boddhisattva's

@boddhisattva是有效点。但是,此规范应用于测试是否调用ServiceMailernew_user方法。创建deliver_later邮件后,您可以自由创建另一个测试来测试所调用的方法。
morgler's

@morgler感谢您对我的问题的答复。我现在了解到,您主要allow根据测试ServiceMailer's new_user方法的上下文建议了的用法。万一我必须进行测试deliver_later,我想我只是在现有测试中添加另一个断言(用于检查ServiceMailer's new_user方法)以检查类似内容,expect(mailer_object).to receive(:deliver_later)而不是将其完全作为另一个测试来进行测试。知道如果为什么我们不得不进行测试的话,为什么会选择单独的测试会很有趣deliver_later
boddhisattva's

44

使用ActiveJob和rspec-rails3.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

但是请记住,这将影响测试套件的整体性能,因为所有作业一入队就将立即运行。


36

如果您发现此问题,但仅使用ActiveJob而不是单独使用DelayedJob,并且使用Rails 5,建议您在config/environments/test.rb以下位置配置ActionMailer :

config.active_job.queue_adapter = :inline

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


运行规范时,它不执行所有异步任务吗?
Aleksey

是的,那是一个好点。这在ActiveJob的简单轻巧用例中非常方便,您可以在其中配置所有异步任务以内联运行,并使测试变得简单。
·科普利

1
可能只是节省了我一个小时的调试时间。谢谢!
马特

这曾经很
不错

27

我将添加我的答案,因为其他任何一个对我来说都不够好:

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。,要么async还是inline发展环境。

给定测试环境的以下配置,您的电子邮件将始终被排队,并且永远不会执行以进行发送:这可防止您嘲笑电子邮件,并且您可以检查电子邮件是否已正确排队。

在您的测试中,请始终将测试分为两部分: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


2
只是必须将班级改为ActionMailer::MailDeliveryJob而不是ActionMailer::DeliveryJob
haffla

这是一个很好的答案!
Holger Frohloff

10

添加:

# spec/support/message_delivery.rb
class ActionMailer::MessageDelivery
  def deliver_later
    deliver_now
  end
end

参考:http//mrlab.sk/testing-email-delivery-with-deliver-later.html


4
这对我有用,但我正在使用deliver_later(wait: 2.minutes)。所以我做到了deliver_later(options={})
rigelstpierre

8
应用程序可以发送同步和异步电子邮件,这是一种黑客行为,无法区分测试之间的差异。
杰里科

2
我同意黑客入侵是一个坏主意。将_later别名为_now只会以痛苦而告终。
John Paul Ashenfelter,2016年

2
该链接已消失,但我在返回机器的途中找到了它。web.archive.org/web/20150710184659/http://www.mrlab.sk/…–
OzBarry

我知道了NameError: uninitialized constant ActionMailer
AlbertCatalà17年

10

一个更好的解决方案(比Monkeypatching 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

5

我也有同样的疑问,并以较少的冗长(单行)方式解决了这个问题

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



3

对于最近的Google员工:

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 < ApplicationController def 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::TestHelper 
  … 
end 

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


0

我来这里寻找一个完整的测试的答案,所以, 只是询问是否有一个邮件等待发送,此外,它的收件人,主题...等

我有一个解决方案,比这里的解决方案要略多一点:

就像说的那样

mail = perform_enqueued_jobs { ActionMailer::DeliveryJob.perform_now(*enqueued_jobs.first[:args]) }

问题在于,在这种情况下,邮件程序接收的参数不同于生产环境中接收的参数,如果生产中的第一个参数是模型,则现在在测试中将收到哈希,因此崩溃

enqueued_jobs.first[:args]
["UserMailer", "welcome_email", "deliver_now", {"_aj_globalid"=>"gid://forjartistica/User/1"}]

因此,如果我们在邮递员UserMailer.welcome_email(@user).deliver_later在生产中收到用户时称其为邮递员,但在测试中会收到{"_aj_globalid"=>"gid://forjartistica/User/1"}

所有评论将不胜感激,我发现的最轻松的解决方案是更改我呼叫邮件程序,传递,模型的ID而不是模型的方式:

UserMailer.welcome_email(@user.id).deliver_later


0

这个答案有些不同,但是在诸如Rails API的新更改或您希望提供的方式发生更改(例如使用deliver_now而不是deliver_later)的情况下可能会有所帮助。

我大部分时间要做的是将邮件程序作为对我正在测试的方法的依赖关系,但是我没有通过rails传递邮件程序,而是通过一个对象来执行“我想要”...

例如,如果我想检查用户注册后是否发送了正确的邮件...我可以这样做...

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

然后在我将要调用该方法的控制器中,我将编写该邮件程序的“真实”实现...

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

通过这种方式,我感觉到我正在测试是否正在使用正确的数据(参数)将正确的消息发送到正确的对象。而且我只需要创建一个没有逻辑的非常简单的对象,只需要了解ActionMailer的调用方式即可。

我更喜欢这样做,因为我更希望对自己拥有的依赖项有更多的控制权。这是“依赖倒置原则”的一个示例。

我不确定这是否符合您的口味,但这是解决问题的另一种方法=)。


0

我认为测试此问题的更好方法之一是与基本响应json检查一起检查工作状态,例如:

expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.on_queue('mailers').with('mailer_name', 'mailer_method', 'delivery_now', { :params => {}, :args=>[] } )
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.