43

I'm working on a reset_password method in a Rails API app. When this endpoint is hit, an ActiveJob is queued that will fire off a request to Mandrill (our transactional email client). I'm currently trying to write the tests to ensure that that the ActiveJob is queued correctly when the controller endpoint is hit.

def reset_password
  @user = User.find_by(email: params[:user][:email])
  @user.send_reset_password_instructions
end

The send_reset_password_instructions creates some url's etc before creating the ActiveJob which's code is below:

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

At the moment we are not using any adapters for the ActiveJob, so I just want to check with Rspec that the ActiveJob is queued.

Currently my test looks something like this (I'm using factory girl to create the user):

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

Everything works in reality, I just need to create the tests!

I'm using ruby 2.1.2 and rails 4.1.6.

I can't see any documentation or help anywhere on the web on how to test on this so any help would be greatly appreciated!

mylescc
  • 5,720
  • 3
  • 17
  • 23

9 Answers9

59

The accepted answer no longer works for me, so I tried Michael H.'s suggestion in the comments, which works.

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
Josh Smith
  • 14,674
  • 18
  • 72
  • 118
  • Thanks, that's a great help. Is there any way to check that the correct job is queued? – bobomoreno Dec 25 '14 at 13:24
  • 13
    The `ActiveJob::TestHelper` ist intended to be used with minitest not rspec. It's code is full of `assert_equal` etc. Including it only for one method is bad idea, imho. The method `enqueued_jobs` within this module is just a shortcut for `ActiveJob::Base.queue_adapter.enqueued_jobs` – dre-hh Feb 09 '15 at 22:12
  • 3
    @bobomoreno `ActiveJob::Base.queue_adapter.enqueued_jobs` will give you access to the particular jobs that have been queued. – Jason Swett Sep 23 '15 at 17:48
  • You can also do `expect { your_action }.to change(enqueued_jobs, :size).by n`, I like to use this to test that no job has been enqueued, using n = 0. You don't even have to do the clear_enqueued_jobs after suite using this method. – Miguel Corti Apr 15 '20 at 17:14
48

In a unit test, instead of checking what is queued one can also rely on ActiveJob working properly and just verify that it will be called by mocking its api.

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

The creators of the ActiveJob have used the same techniques for their unit tests. See GridJob Testobject

They create a testmock GridJob in their tests and override the perform method, so that it only adds jobs to a custom Array, they call JobBuffer. At the end they test, whether the buffer has jobs enqueued

At a single place one can ofc also do an integrations test. The ActiveJob test_helper.rb is supposed to be used with minitest not with rspec. So you have to rebuild it's functionalitity. You can just call

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

without requiring anything

Update 1: As noticed within a comment. ActiveJob::Base.queue_adapter.enqueued_jobs works only by setting it the queue_adapter into test mode.

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

# or within a test setup
ActiveJob::Base.queue_adapter = :test
dre-hh
  • 7,840
  • 2
  • 33
  • 44
  • So I ended up doing your first suggestion. The ActiveJob test_helper doesn't work with rspec annoyingly – mylescc Oct 10 '14 at 13:41
  • 2
    @mylescc I was able to get the test_helper working by including it in my Rspec.describe block: `include ActiveJob::TestHelper` – Miriam H. Oct 13 '14 at 16:47
  • not sure still didn't work for me. I have tried including inside describe and also tried in configure block as well. – joe nayyar Nov 05 '14 at 17:01
  • 3
    ActiveJob::Base.queue_adapter.enqueued_jobs doesn't work anymore =( – William Weckl Dec 04 '14 at 16:19
  • 3
    Testing against `ActiveJob::Base.queue_adapter.enqueued_jobs` works as long as `config.active_job.queue_adapter = :test` is in your config/environments/test.rb file. – Chris Feb 09 '15 at 22:02
  • 3
    `expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to eq 1` should be `expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq 1`, missing `.size`. – Juanito Fatas Mar 29 '15 at 07:41
  • 2
    The example spec you used won't actually work. The expectation needs to appear before the method that propagates the call. – Noz Apr 30 '15 at 22:21
21

Rspec 3.4 now has have_enqueued_job cooked in, which makes this a lot easier to test:

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

it has other niceties for have_enqueued_job to allow you to check the argument(s) and the number of times it should be queued up.

tirdadc
  • 4,603
  • 3
  • 38
  • 45
12

Testing Rails ActiveJob with 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
ChuckJHardy
  • 6,956
  • 6
  • 35
  • 39
9

There is a new rspec extension which makes your life easier.

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
KARASZI István
  • 30,900
  • 8
  • 101
  • 128
6

In my opinion, ensure a job was enqueued when a request is performed is important. You can do it with the below solutions:

Solution 1

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

Solution 2

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

giapnh
  • 2,950
  • 24
  • 20
5

I had some problems, maybe because I didn't include ActiveJob::TestHelper, but this worked for me...

Firstly ensure, that you have the queue adapter set to :test as above answers show.

For some reason clear_enqueued_jobs jobs in the after block didn't work for me, but the source shows we can do the following: 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
Archernar
  • 463
  • 5
  • 6
0

I think the solutions using expect { your_code }.to have_enqueued_job(YourJob) to be very clean, since they use the "official" assertions. If you do not like long blocks passed to expect, you can also use:

YourJob.perform_later
expect(YourJob).to have_been_enqueued

Please find good examples in the rubydoc documentation.

Motine
  • 1,638
  • 18
  • 18
0

A simple solution is

# 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

then you can use helper method my_jobs in test like

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
Shiva
  • 11,485
  • 2
  • 67
  • 84