如何使用rspec测试ActionMailer deliver_later

时间:2014-12-25 13:45:13

标签: ruby-on-rails rspec actionmailer delayed-job rails-activejob

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

我试图测试新的' deliver_later'使用Rspec的方法,但我不确定如何。

旧控制器代码:

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

现在我使用它会出错。 (Double" mailer"收到意外消息:deliver_later with(no args))

只需

expect(ServiceMailer).to receive(:new_user)
使用' undefined方法`deliver_later'

也失败了。为零:NilClass'

我已尝试过一些示例,可以让您查看作业是否在ActiveJob中使用test_helper排队,但我还没有设法测试正确的作业是否排队。

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

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

我想做的是:

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

任何想法? 感谢

13 个答案:

答案 0 :(得分:68)

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

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 :(得分:32)

如果你发现这个问题但是使用的是ActiveJob而不是简单的DelayedJob,并且正在使用Rails 5,我建议在import Cocoa import PlaygroundSupport let image = NSImage(named: "earth.gif") let imageView = NSImageView(image: image!) imageView.frame = CGRect(x: 0, y: 0, width: 200.0, height: 200.0) PlaygroundPage.current.liveView = imageView 中配置ActionMailer:

config/environments/test.rb

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

答案 2 :(得分:31)

使用ActiveJob和rspec 3.4+,您可以使用have_enqueued_job,如下所示:

expect {
  YourMailer.your_method.deliver_later
}.to have_enqueued_job.on_queue('mailers')

答案 3 :(得分:9)

我会添加我的答案,因为其他人对我来说都不够好:

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

答案 4 :(得分:8)

添加:

# 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

答案 5 :(得分:7)

一个更好的解决方案(比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) }确保在检查测试值之前运行后台任务。

答案 6 :(得分:3)

我带着同样的怀疑,以this answer

为灵感,以一种不那么冗长(单行)的方式解决
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

答案 7 :(得分:2)

一种简单的方法是:

expect(ServiceMailer).to(
  receive(:new_user).with(@user).and_call_original
)
# subject

答案 8 :(得分:0)

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

我有一个解决方案,而不是来自here,但有一点变化:

正如它所说的那样,curial部分是

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

问题是,在这种情况下,参数比邮件程序接收的参数不同于生产中接收的参数,如果第一个参数是Model,现在在测试中会收到一个哈希,所以会崩溃< / p>

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

答案 9 :(得分:0)

这个答案略有不同,但可能会对rails API中的新更改或您希望提供的方式发生更改(例如使用deliver_now而不是deliver_later)有所帮助)。

我大部分时间做的是将邮件作为依赖关系传递给我正在测试的方法,但是我没有从rails传递邮件,而是传递了一个可以执行操作的对象以我想要的方式&#34; ...

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

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

然后在我将调用该方法的控制器中,我会编写&#34; real&#34;该邮件的实施......

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。

我更喜欢这样做,因为我更喜欢控制我拥有的依赖项。这是我"Dependency inversion principle"的一个例子。

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

答案 10 :(得分:0)

此答案适用于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

答案 11 :(得分:0)

对于最近的Google员工:

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

expect(YourMailer).to have_received(:mailer_method)

答案 12 :(得分:0)

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

expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.on_queue('mailers').with('mailer_name', 'mailer_method', 'delivery_now', { :params => {}, :args=>[] } )