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!