ChatGPT解决这个技术问题 Extra ChatGPT

如何使用 rspec 测试 ActionMailer Deliver_later

尝试使用delayed_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))

现在我使用它会出错。 (双“邮件程序”收到意外消息:deliver_later with (no args))

只是

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)

有任何想法吗??谢谢


f
fqxp

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

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 同意。我更新了答案。感谢您的关注/评论。
这可能有点离题@morgler,但我很想知道你或其他人的一般想法,如果出于某种原因(比如错误地)说,deliver_later 方法从控制器中删除,使用 allow我们将无法捕捉到对吗?我的意思是测试仍然会通过。您仍然认为使用 allow 会比使用 expect 更好吗?如果 deliver_later 被错误删除,我确实看到了 expect 标志这就是为什么我想大体上讨论这个问题的原因。如果您能详细说明为什么 allow 在上述上下文中更好,那就太好了。
@boddhisattva 一个有效的观点。但是,该规范应该测试是否调用了 ServiceMailernew_user 方法。创建邮件后,您可以自由地创建另一个测试来测试被调用的 deliver_later 方法。
@morgler 感谢您对我的问题的答复。我现在了解到您主要是根据测试 ServiceMailer's new_user 方法的上下文建议使用 allow。如果我必须测试 deliver_later,我想我只需在现有测试(检查 ServiceMailer's new_user 方法)中添加另一个断言来检查 expect(mailer_object).to receive(:deliver_later) 之类的东西,而不是将其作为另一个测试完全测试.如果我们不得不测试 deliver_later,那么知道为什么您更愿意为此单独测试会很有趣。
A
Alter Lagos

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

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


现在可能应该检查 have_enqueued_mailrelishapp.com/rspec-staging/rspec-rails/docs/matchers/…
G
Gabe Kopley

如果您发现这个问题,但使用的是 ActiveJob 而不是简单地单独使用 DelayedJob,并且使用的是 Rails 5,我建议在 config/environments/test.rb 中配置 ActionMailer:

config.active_job.queue_adapter = :inline

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


它不会在运行规范时执行所有异步任务吗?
是的,它会的,这是一个很好的观点。这在 ActiveJob 的简单轻量用例中很方便,您可以将所有异步任务配置为内联运行,并且使测试变得简单。
可能只是为我节省了一个小时的调试时间。谢谢!
这曾经很好地工作,但似乎在最近的捆绑更新中停止了:( - 有什么想法吗?
c
coorasse

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

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 = :asyncasyncinline 用于开发环境。

给定测试环境的以下配置,您的电子邮件将始终排队并且永远不会执行交付:这可以防止您模拟它们并且您可以检查它们是否正确排队。

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


只需将类更改为 ActionMailer::MailDeliveryJob 而不是 ActionMailer::DeliveryJob
这是一个很好的答案!
谢谢!在 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)
作为此方法的一种变体,您可以使用 have_enqueued_mail 匹配器进行检查,请参阅 relishapp.com/rspec/rspec-rails/v/5-0/docs/matchers/…
M
Minimul

添加这个:

# 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


这对我有用,但我使用的是 deliver_later(wait: 2.minutes)。所以我做了deliver_later(options={})
应用程序可以发送同步和异步电子邮件,这是一种无法区分测试的技巧。
我同意黑客是一个坏主意。将 _later 别名为 _now 只会以痛苦告终。
该链接已失效,但我在回程机器上找到了它; web.archive.org/web/20150710184659/http://www.mrlab.sk/…
我得到NameError: uninitialized constant ActionMailer
Q
Qqwy

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


这种方法肯定更易于理解,但如果主题操作将任何耗时的工作排入队列,则可能会大大减慢您的测试速度。
这也不会测试正在选择哪个邮件/操作。如果您的代码涉及有条件地选择不同的邮件,那将无济于事
C
Community

我有同样的疑问,并以一种不太冗长(单行)的方式解决了 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


N
Nuno Silva

一个简单的方法是:

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

m
maurymmarques

这个答案是针对 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


c
crowns4days

对于最近的 Google 员工:

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

expect(YourMailer).to have_received(:mailer_method)

s
sabhari karthik

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

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

A
Albert Català

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

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

正如它所说,curial部分是

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

问题是mailer接收的参数,在这种情况下,与生产中接收的参数不同,生产中,如果第一个参数是模型,现在在测试中会收到一个哈希,所以会崩溃

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


B
Benito Serna

这个答案有点不同,但在 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。

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

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


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅