如何使用 Rspec 检查在 ActiveJob 中排队的内容

45

我正在一个Rails API应用中开发reset_password方法。当这个端点被访问时,会排队执行一个ActiveJob,该任务将发送一个请求到Mandrill(我们的事务性电子邮件客户端)。我目前正在尝试编写测试以确保在控制器端点被访问时正确地排队ActiveJob。

def reset_password
  @user = User.find_by(email: params[:user][:email])
  @user.send_reset_password_instructions
end
send_reset_password_instructions在创建ActiveJob之前会创建一些URL等内容,下面是ActiveJob的代码:
class SendEmailJob < ActiveJob::Base
  queue_as :default

  def perform(message)
    mandrill = Mandrill::API.new
    mandrill.messages.send_template "reset-password", [], message
  rescue Mandrill::Error => e
    puts "A mandrill error occurred: #{e.class} - #{e.message}"
    raise
  end
end

目前我们没有为ActiveJob使用任何适配器,所以我只想用Rspec检查ActiveJob是否已经排队。

目前我的测试大概像这样(我正在使用factory girl创建用户):

require 'active_job/test_helper'

describe '#reset_password' do
  let(:user) { create :user }

  it 'should create an ActiveJob to send the reset password email' do
    expect(enqueued_jobs.size).to eq 0
    post :reset_password, user: { email: user.email }
    expect(enqueued_jobs.size).to eq 1
  end
end

实际情况下一切正常,我只需要创建测试!

我正在使用 Ruby 2.1.2 和 Rails 4.1.6。

我无法在网上找到任何关于如何进行此类测试的文档或帮助,因此非常感激任何帮助!

9个回答

61

原先被接受的答案对我已经不再适用了,所以我尝试了迈克尔H.在评论中提出的建议,它有效。

describe 'whatever' do
  include ActiveJob::TestHelper

  after do
    clear_enqueued_jobs
  end  

  it 'should email' do
    expect(enqueued_jobs.size).to eq(1)
  end
end

谢谢,这非常有帮助。有没有办法检查正确的作业是否已排队? - bobomoreno
13
ActiveJob::TestHelper 旨在与 minitest 一起使用,而不是 rspec。它的代码充满了 assert_equal 等语句。仅为一个方法包含它是个坏主意,我认为。此模块中的 enqueued_jobs 方法只是 ActiveJob::Base.queue_adapter.enqueued_jobs 的快捷方式。 - dre-hh
3
ActiveJob::Base.queue_adapter.enqueued_jobs 可以让你访问已经被排队的具体作业。 - Jason Swett
你也可以使用 expect { your_action }.to change(enqueued_jobs, :size).by n,我喜欢使用这种方式来测试没有作业被加入队列的情况,其中 n = 0。使用此方法后,您甚至不必在套件之后清除 enqueued_jobs。 - Miguel Corti

49

在单元测试中,可以不检查队列中的内容,而是依靠 ActiveJob 正常工作,并通过模拟其 API 来验证它是否被调用。

 expect(MyJob).to receive(:perform_later).once 
 post :reset_password, user: { email: user.email }

ActiveJob的创建者在其单元测试中使用了相同的技术。请参阅GridJob Testobject

他们在测试中创建了一个名为testmock GridJob的模拟对象,并覆盖了perform方法,使其只向自定义数组添加作业,称为JobBuffer。最后,他们测试缓冲区是否有作业已入队。

当然,也可以在一个地方进行集成测试。ActiveJob的test_helper.rb应该与minitest一起使用,而不是rspec。因此,您需要重建它的功能。只需调用:

expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to eq 1

不需要任何东西

更新1: 如评论中所指出的那样。 ActiveJob::Base.queue_adapter.enqueued_jobs 只有在将 queue_adapter 设为测试模式时才起作用。

# either within config/environment/test.rb
config.active_job.queue_adapter = :test

# or within a test setup
ActiveJob::Base.queue_adapter = :test

2
通过将ActiveJob::TestHelper包含在我的Rspec.describe块中,我成功地让test_helper工作了:include ActiveJob::TestHelper - Miriam H.
3
ActiveJob::Base.queue_adapter.enqueued_jobs不再起作用=((翻译说明:该句为一段程序代码,在程序开发过程中出现问题,表示该部分功能无法正常工作。) - William Weckl
3
只要在config/environments/test.rb文件中设置config.active_job.queue_adapter = :test,针对ActiveJob::Base.queue_adapter.enqueued_jobs进行测试就可以了。 - Chris
3
应该将语句改为 expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq 1,因为缺少 .size - Juanito Fatas
2
你使用的示例规范实际上不起作用。期望值需要出现在传播调用的方法之前。 - Noz
显示剩余2条评论

21

Rspec 3.4现在内置了have_enqueued_job,这使得测试变得更加容易:

it "enqueues a YourJob" do
  expect {
    get :your_action, {}
  }.to have_enqueued_job(YourJob)
end

它还有其他一些好处供have_enqueued_job使用,让你可以检查参数和应该排队的次数。


6
有用的人可以使用 expect { }.to have_enqueued_job.on_queue('mailers') 来检查已发送的电子邮件。 - Alter Lagos
优秀的推荐! - Martin Streicher

12

测试Rails ActiveJob与RSpec

class MyJob < ActiveJob::Base
  queue_as :urgent

  rescue_from(NoResultsError) do
    retry_job wait: 5.minutes, queue: :default
  end

  def perform(*args)
    MyService.call(*args)
  end
end

require 'rails_helper'

RSpec.describe MyJob, type: :job do
  include ActiveJob::TestHelper

  subject(:job) { described_class.perform_later(123) }

  it 'queues the job' do
    expect { job }
      .to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1)
  end

  it 'is in urgent queue' do
    expect(MyJob.new.queue_name).to eq('urgent')
  end

  it 'executes perform' do
    expect(MyService).to receive(:call).with(123)
    perform_enqueued_jobs { job }
  end

  it 'handles no results error' do
    allow(MyService).to receive(:call).and_raise(NoResultsError)

    perform_enqueued_jobs do
      expect_any_instance_of(MyJob)
        .to receive(:retry_job).with(wait: 10.minutes, queue: :default)

      job
    end
  end

  after do
    clear_enqueued_jobs
    clear_performed_jobs
  end
end

9

现在有一个新的rspec扩展,可以让您的生活更加轻松。

require 'rails_helper'

RSpec.describe MyController do
  let(:user) { FactoryGirl.create(:user) }
  let(:params) { { user_id: user.id } }
  subject(:make_request) { described_class.make_request(params) }

  it { expect { make_request }.to enqueue_a(RequestMaker).with(global_id(user)) }
end

6

在我看来,当执行请求时确保作业已排队非常重要。您可以通过以下解决方案实现:

解决方案 1

expect{ post your_api_here, params: params, headers: headers }
 .to have_enqueued_job(YourJob)
 .with(args)

解决方案2

expect(YourJob).to receive(:perform_later).once.with(args)
post your_api_here, params: params, headers: headers


5

我遇到了一些问题,可能是因为我没有包含ActiveJob :: TestHelper,但这个方法对我有效...

首先确保您已将队列适配器设置为:test,就像上面的答案所显示的那样。

由于某种原因,在after块中使用clear_enqueued_jobs无法正常工作,但 source 显示我们可以执行以下操作:enqueued_jobs.clear

require 'rails_helper'
include RSpec::Rails::Matchers

RSpec.describe "my_rake_task", type: :rake do

  after do
    ActiveJob::Base.queue_adapter.enqueued_jobs.clear
  end  


  context "when #all task is run" do
    it "enqueues jobs which have been enabled" do
      enabled_count = get_enabled_count
      subject.execute
      expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq(enabled_count)
    end

    it "doesn't enqueues jobs which have been disabled" do
      enabled_count = get_enabled_count
      subject.execute
      expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq(enabled_count)
    end
  end

end

0
一个简单的解决方案是:
# frozen_string_literal: true

class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
  #

  def self.my_jobs
    enqueued_jobs.select{|x| x['job_class'] == self.name}
  end
end

然后你可以在测试中使用帮助方法my_jobs

require 'rails_helper'

RSpec.describe SendBookingRemindersJob, type: :job do
  describe '.start_time_approaching' do
      let!(:booking) { create :booking } 

      it 'schedules 4 jobs' do
        SendBookingRemindersJob.start_time_approaching(booking)
        expect(SendBookingRemindersJob.my_jobs.count).to eq(4)
      end
  end

0

我认为使用expect { your_code }.to have_enqueued_job(YourJob)的解决方案非常干净,因为它们使用了“官方”的断言。如果您不喜欢传递给expect的长块,您也可以使用:

YourJob.perform_later
expect(YourJob).to have_been_enqueued

请在{{link1:rubydoc文档}}中查找好的示例。

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