Netskin Logo

Test ActiveJob Retry Logic in RSpec

#rails
#rspec
by Ingo Albers on 30.06.2022

When testing ActiveJob jobs in RSpec, usually you focus on the functionality of the job - To validate that the job does what it is supposed to do. To easily do this the queue adapter can be set to inline for example directly in the test.rb environment file.

# config/environments/test.rb
Rails.application.configure do
  config.active_job.queue_adapter = :inline
end

This works well for the happy cases, when no problems occur. But what if you want to test the error handling and the retry logic? In this case we would want to actually test the real processing of the job, as it would also happen on a production environment. To do this we introduce an easy way to configure specific jobs to use the delayed_job adapter.

# spec/rails_helper.rb
RSpec.configure do |config|
  config.around(:each, :active_job_delayed_job_adapter) do |example|
    original_adapter = ActiveJob::Base.queue_adapter
    ActiveJob::Base.queue_adapter = :delayed_job
    example.run
    ActiveJob::Base.queue_adapter = original_adapter
  end

  config.include(ActiveJobTestHelper)
end

This will then overwrite the default queue adapter for a specific example just by calling the spec with the configuration option and resetting it afterwards.

Additionally, we introduce a method that works off the delayed jobs. By default this will initialize a worker and work off all enqueued jobs and also reschedule them immediately in case of an error.

# spec/rails_helper.rb
module ActiveJobTestHelper
  def work_off_delayed_jobs(count = Delayed::Worker.max_attempts + 1)
    # Reschedule job immediately on failure so it is handled by the next work_off
    allow_any_instance_of(ApplicationJob).to receive(:reschedule_at) do |current_time, attempts|
      current_time
    end

    count.times do
      Delayed::Worker.new.work_off
    end
  end
end

As an example we create a very simple ActiveJob class:

# app/jobs/basic_job.rb
class BasicJob < ApplicationJob
  def perform
    # Do something later
  end
end

In our RSpec test we then have the possibility to easily set the desired queue adapter. This will make sure that the execution of the job as well as the the error and retry handling are processed in the same way as in our production environment.

# spec/jobs/basic_job_spec.rb
RSpec.describe BasicJob do
  describe "#perform_later" do
    context "error handling", :active_job_delayed_job_adapter do
      before do
        allow_any_instance_of(described_class).to receive(:perform).and_raise(StandardError)
      end

      it "tries to run the job for default attempt count" do
        expect{ described_class.perform_later.to change{ Delayed::Job.count }.by(1)
        job = Delayed::Job.last
        expect{ work_off_delayed_jobs! }.to change{ job.reload.attempts }.to(Delayed::Worker.max_attempts)
      end
    end
  end
end

This can be extended to test specific behaviour of jobs in case of an error, which can be very useful if you have defined custom retry logic or error handling that would only be triggered after multiple failed attempts.

Happy Coding!

Resources

❮ Copy to Clipboard with Javascript
How to Use Heredoc in Ruby ❯
Netskin Logo