Sidekiq Testing Patterns in Rails: Unit, Integration, and System Tests

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
end

Defining 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
end

Unit 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
end

Unit 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
end

Testing 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
end

ActiveJob 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
end

Integration 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
end
RSpec.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
end

Testing 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
end

Testing 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
end

System 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
end

What 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.

Read more