Testing Sidekiq Workers: Unit Tests, Fake Mode, and Integration Patterns
Sidekiq is the standard background job processor for Ruby/Rails. Its built-in testing support includes fake queues, inline execution, and RSpec matchers that make worker testing straightforward.
Testing Modes
Sidekiq provides three testing modes via sidekiq/testing:
| Mode | Behavior |
|---|---|
Sidekiq::Testing.fake! |
Jobs enqueued but not executed (default for tests) |
Sidekiq::Testing.inline! |
Jobs execute immediately when enqueued |
Sidekiq::Testing.real! |
Normal Redis-backed behavior |
Setup
# spec/spec_helper.rb or spec/rails_helper.rb
require 'sidekiq/testing'
RSpec.configure do |config|
config.before(:each) do
Sidekiq::Testing.fake!
end
config.after(:each) do
Sidekiq::Worker.clear_all
end
endUnit Testing Worker Logic
Extract business logic from the worker to test it in isolation:
# app/workers/welcome_email_worker.rb
class WelcomeEmailWorker
include Sidekiq::Worker
sidekiq_options queue: :mailers, retry: 3
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
user.update!(welcome_email_sent_at: Time.current)
end
end# spec/workers/welcome_email_worker_spec.rb
require 'rails_helper'
RSpec.describe WelcomeEmailWorker, type: :worker do
describe '#perform' do
let(:user) { create(:user) }
it 'sends welcome email to user' do
expect(UserMailer).to receive(:welcome).with(user).and_return(
double(deliver_now: true)
)
described_class.new.perform(user.id)
end
it 'updates welcome_email_sent_at timestamp' do
allow(UserMailer).to receive(:welcome).and_return(double(deliver_now: true))
expect {
described_class.new.perform(user.id)
}.to change { user.reload.welcome_email_sent_at }.from(nil).to(be_within(2.seconds).of(Time.current))
end
it 'raises ActiveRecord::RecordNotFound for missing user' do
expect {
described_class.new.perform(0)
}.to raise_error(ActiveRecord::RecordNotFound)
end
end
endTesting Enqueuing with Fake Mode
In fake mode, assert that jobs were enqueued without executing them:
RSpec.describe 'User registration', type: :request do
it 'enqueues welcome email job after signup' do
expect {
post '/users', params: { user: { email: 'alice@example.com', password: 'secret' } }
}.to change(WelcomeEmailWorker.jobs, :size).by(1)
job = WelcomeEmailWorker.jobs.last
expect(job['args']).to include(User.last.id)
expect(job['queue']).to eq('mailers')
end
it 'enqueues with correct retry count' do
post '/users', params: { user: { email: 'bob@example.com', password: 'secret' } }
job = WelcomeEmailWorker.jobs.last
expect(job['retry']).to eq(3)
end
endUsing RSpec Matchers
# With sidekiq-rspec matchers (gem 'rspec-sidekiq')
RSpec.describe UsersController, type: :controller do
describe 'POST #create' do
it 'enqueues welcome email' do
expect {
post :create, params: { user: { email: 'alice@example.com' } }
}.to enqueue_sidekiq_job(WelcomeEmailWorker)
end
it 'enqueues with user ID argument' do
post :create, params: { user: { email: 'alice@example.com' } }
expect(WelcomeEmailWorker).to have_enqueued_sidekiq_job(User.last.id)
end
end
endTesting Scheduled Jobs
# app/workers/report_worker.rb
class ReportWorker
include Sidekiq::Worker
def perform(report_type)
Report.generate(report_type)
end
end
# spec/workers/report_worker_spec.rb
it 'schedules report for 1 hour from now' do
expect {
ReportWorker.perform_in(1.hour, 'daily')
}.to change(ReportWorker.jobs, :size).by(1)
job = ReportWorker.jobs.last
scheduled_at = Time.at(job['at'])
expect(scheduled_at).to be_within(5.seconds).of(1.hour.from_now)
end
it 'schedules cron-based job' do
Sidekiq::Cron::Job.create(
name: 'daily-report',
cron: '0 9 * * *',
class: 'ReportWorker',
args: ['daily']
)
job = Sidekiq::Cron::Job.find('daily-report')
expect(job).to be_present
expect(job.cron).to eq('0 9 * * *')
endInline Mode for Integration Tests
RSpec.describe 'Order processing', type: :request do
around do |example|
Sidekiq::Testing.inline! { example.run }
end
it 'sends invoice email immediately after order' do
# With inline mode, PaymentWorker executes synchronously
expect(InvoiceMailer).to receive(:order_confirmation).and_return(
double(deliver_later: true)
)
post '/orders', params: { order: { product_id: 1, quantity: 2 } }
expect(response).to have_http_status(:created)
end
endTesting Retries
RSpec.describe PaymentWorker do
describe 'retry behavior' do
it 'retries on Stripe::RateLimitError' do
allow(Stripe::Charge).to receive(:create).and_raise(Stripe::RateLimitError)
# The worker should raise, Sidekiq will retry
expect {
described_class.new.perform('ord-1', 9999)
}.to raise_error(Stripe::RateLimitError)
end
it 'does not retry on permanent errors' do
# Mark specific errors as non-retryable
allow(Stripe::Charge).to receive(:create).and_raise(
Stripe::CardError.new('Card declined', nil, 'card_declined')
)
expect {
described_class.new.perform('ord-1', 9999)
}.to raise_error(Stripe::CardError)
# Verify the order is marked failed, not retried
expect(Order.find('ord-1').status).to eq('payment_failed')
end
end
endTesting Dead Job Sets
it 'moves job to dead set after exhausting retries' do
Sidekiq::Testing.real! do
# Inject a failing job directly
Sidekiq::Client.push(
'class' => 'WelcomeEmailWorker',
'args' => [999],
'retry' => 0 # No retries
)
# Process it
Sidekiq::Processor.new(nil, nil).process_one
dead = Sidekiq::DeadSet.new
expect(dead.size).to eq(1)
expect(dead.first['class']).to eq('WelcomeEmailWorker')
end
endTesting ActiveJob with Sidekiq Adapter
# config/application.rb
config.active_job.queue_adapter = :sidekiq
# spec/jobs/invoice_job_spec.rb
RSpec.describe InvoiceJob, type: :job do
it 'queues job to invoices queue' do
expect {
InvoiceJob.perform_later(order_id: 'ord-1')
}.to have_enqueued_mail(InvoiceMailer, :send_invoice)
# Or with rspec-sidekiq:
# }.to enqueue_sidekiq_job(InvoiceJob).on_queue('invoices')
end
endSummary
Sidekiq testing is well-supported:
Sidekiq::Testing.fake!— fast unit tests, assert on enqueued jobsSidekiq::Testing.inline!— integration tests, execute jobs synchronously- Direct
worker.perform(args)— test handler logic in isolation without any queue
Use fake mode as the default. Switch to inline for tests that need to verify the full request→background job→side effect chain. Always call Sidekiq::Worker.clear_all in after(:each) to prevent job leakage between tests.