Sidekiq Testing Patterns in Rails: Unit, Integration, and System Tests
Sidekiq is the standard background job processor for Ruby on Rails. Testing Sidekiq workers well requires understanding three modes: fake (in-memory queue, no Redis), inline (synchronous execution), and real (full Redis-backed queue). Each mode serves a different testing layer.
Setup
# Gemfile
gem 'sidekiq'
gem 'sidekiq-testing', group: :test# spec/support/sidekiq.rb
require 'sidekiq/testing'
RSpec.configure do |config|
config.before(:each) do
Sidekiq::Testing.fake!
Sidekiq::Worker.clear_all
end
endDefining a Worker
# app/workers/welcome_email_worker.rb
class WelcomeEmailWorker
include Sidekiq::Worker
sidekiq_options queue: :emails, retry: 3, backtrace: true
def perform(user_id, email)
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
Rails.logger.info "Welcome email sent to #{email}"
end
endUnit Tests: Fake Mode
In fake mode, perform_async pushes jobs onto an in-memory queue without executing them. This is the right layer for testing that jobs are enqueued with correct arguments:
# spec/workers/welcome_email_worker_spec.rb
require 'rails_helper'
RSpec.describe WelcomeEmailWorker, type: :worker do
describe 'job enqueueing' do
it 'enqueues a welcome email job' do
expect {
WelcomeEmailWorker.perform_async('user-1', 'alice@example.com')
}.to change(WelcomeEmailWorker.jobs, :size).by(1)
end
it 'enqueues with the correct arguments' do
WelcomeEmailWorker.perform_async('user-1', 'alice@example.com')
job = WelcomeEmailWorker.jobs.last
expect(job['args']).to eq(['user-1', 'alice@example.com'])
end
it 'enqueues to the emails queue' do
WelcomeEmailWorker.perform_async('user-1', 'alice@example.com')
job = WelcomeEmailWorker.jobs.last
expect(job['queue']).to eq('emails')
end
it 'clears jobs between examples' do
# Relies on before(:each) in spec/support/sidekiq.rb
expect(WelcomeEmailWorker.jobs).to be_empty
end
end
endUnit Tests: Worker Logic with Inline Mode
In inline mode, perform_async executes the job immediately. Use this to test the worker's perform method:
describe 'job execution' do
before { Sidekiq::Testing.inline! }
after { Sidekiq::Testing.fake! }
let(:user) { create(:user, email: 'alice@example.com') }
it 'sends a welcome email to the user' do
expect(UserMailer).to receive(:welcome).with(user).and_call_original
expect_any_instance_of(ActionMailer::MessageDelivery)
.to receive(:deliver_now)
WelcomeEmailWorker.perform_async(user.id, user.email)
end
it 'raises if the user does not exist' do
expect {
WelcomeEmailWorker.perform_async('nonexistent-id', 'x@example.com')
}.to raise_error(ActiveRecord::RecordNotFound)
end
endTesting Worker Logic Directly
For pure unit tests that don't touch Redis at all, call perform directly:
describe '#perform' do
let(:user) { create(:user, email: 'alice@example.com') }
let(:worker) { described_class.new }
it 'delivers a welcome email' do
mail = double('mail', deliver_now: true)
allow(UserMailer).to receive(:welcome).with(user).and_return(mail)
worker.perform(user.id, user.email)
expect(UserMailer).to have_received(:welcome).with(user)
expect(mail).to have_received(:deliver_now)
end
it 'logs the email send' do
mail = double('mail', deliver_now: true)
allow(UserMailer).to receive(:welcome).and_return(mail)
allow(Rails.logger).to receive(:info)
worker.perform(user.id, user.email)
expect(Rails.logger).to have_received(:info).with(/Welcome email sent/)
end
endActiveJob Integration
If you use ActiveJob as a Sidekiq adapter:
# config/application.rb
config.active_job.queue_adapter = :sidekiq# app/jobs/provision_account_job.rb
class ProvisionAccountJob < ApplicationJob
queue_as :default
def perform(user_id)
AccountProvisioner.new(user_id).run
end
end# spec/jobs/provision_account_job_spec.rb
require 'rails_helper'
RSpec.describe ProvisionAccountJob, type: :job do
include ActiveJob::TestHelper
let(:user) { create(:user) }
it 'queues the job' do
expect {
ProvisionAccountJob.perform_later(user.id)
}.to have_enqueued_job(ProvisionAccountJob).with(user.id)
end
it 'provisions the account' do
provisioner = instance_double(AccountProvisioner, run: true)
allow(AccountProvisioner).to receive(:new).with(user.id).and_return(provisioner)
perform_enqueued_jobs do
ProvisionAccountJob.perform_later(user.id)
end
expect(provisioner).to have_received(:run)
end
it 'retries on transient errors' do
allow(AccountProvisioner).to receive(:new).and_raise(Net::OpenTimeout)
expect {
perform_enqueued_jobs do
ProvisionAccountJob.perform_later(user.id)
end
}.to raise_error(Net::OpenTimeout)
end
endIntegration Tests with Real Redis
For integration tests that need real Redis:
# spec/support/sidekiq_real.rb
RSpec.configure do |config|
config.around(:each, :sidekiq_real) do |example|
Sidekiq::Testing.disable!
Sidekiq::Queue.all.each(&:clear)
example.run
Sidekiq::Queue.all.each(&:clear)
Sidekiq::Testing.fake!
end
endRSpec.describe 'Background job pipeline', :sidekiq_real do
it 'processes a welcome email job end-to-end' do
user = create(:user)
WelcomeEmailWorker.perform_async(user.id, user.email)
queue = Sidekiq::Queue.new('emails')
expect(queue.size).to eq(1)
# Process the job using a real worker
worker = Sidekiq::Worker.new
queue.each { |job| WelcomeEmailWorker.new.perform(*job.args) }
# Alternatively, drain in integration tests:
WelcomeEmailWorker.drain
end
endTesting Scheduled Jobs (perform_in, perform_at)
describe 'scheduled jobs' do
it 'schedules a job for the future' do
WelcomeEmailWorker.perform_in(1.hour, 'user-1', 'alice@example.com')
job = WelcomeEmailWorker.jobs.last
expect(job['at']).to be_within(5).of(1.hour.from_now.to_f)
end
it 'schedules a job for a specific time' do
scheduled_at = 2.hours.from_now
WelcomeEmailWorker.perform_at(scheduled_at, 'user-1', 'alice@example.com')
job = WelcomeEmailWorker.jobs.last
expect(job['at']).to be_within(1).of(scheduled_at.to_f)
end
endTesting Retry Configuration
describe 'retry configuration' do
it 'has retry configured to 3' do
options = WelcomeEmailWorker.sidekiq_options
expect(options['retry']).to eq(3)
end
it 'uses the emails queue' do
expect(WelcomeEmailWorker.sidekiq_options['queue']).to eq('emails')
end
endSystem Tests with Sidekiq
In system tests (Capybara), use perform_enqueued_jobs or set the queue adapter to :test:
# spec/system/user_registration_spec.rb
RSpec.describe 'User registration', type: :system do
include ActiveJob::TestHelper
it 'sends a welcome email after signup' do
perform_enqueued_jobs do
visit new_user_registration_path
fill_in 'Email', with: 'alice@example.com'
fill_in 'Password', with: 'securepassword123'
click_button 'Sign up'
end
expect(ActionMailer::Base.deliveries.last.to).to include('alice@example.com')
end
endWhat Automated Tests Miss
RSpec + Sidekiq fake/inline covers job logic but won't catch:
- Real Redis connection issues under load
- Job serialisation failures with complex Ruby objects
- Rack middleware conflicts that prevent jobs from being enqueued
- Worker process crashes from memory bloat on long queues
HelpMeTest runs scheduled E2E tests against your staging app, triggering the full request → job → email pipeline in a real browser — the same path your users take. Sign up free and run your first test in minutes.
Summary
Sidekiq testing modes:
| Mode | Use for |
|---|---|
Sidekiq::Testing.fake! |
Assert job is enqueued, args are correct |
Sidekiq::Testing.inline! |
Test perform logic with all dependencies real |
described_class.new.perform(...) |
Pure unit test of worker logic, no Redis |
| Real Redis | End-to-end queue lifecycle, retry exhaustion |
Always clear jobs in before(:each) with Sidekiq::Worker.clear_all. Use ActiveJob::TestHelper's perform_enqueued_jobs in system tests to flush queues within the test transaction.