RSpec for Sidekiq
Simple testing of Sidekiq jobs via a collection of matchers and helpers.
Jump to Matchers » | Jump to Helpers »
# Gemfile
group :test do
gem 'rspec-sidekiq'
endrspec-sidekiq requires sidekiq/testing by default so there is no need to include the line require "sidekiq/testing" inside your spec_helper.rb.
[!IMPORTANT] This has the effect of not pushing enqueued jobs to Redis but to a
jobarray to enable testing (see the FAQ & Troubleshooting Wiki page). Thus, only includegem "rspec-sidekiq"in environments where this behaviour is required, such as thetestgroup.*
If you wish to modify the default behaviour, add the following to your spec_helper.rb file
RSpec::Sidekiq.configure do |config|
# Clears all job queues before each example
config.clear_all_enqueued_jobs = true # default => true
# Whether to use terminal colours when outputting messages
config.enable_terminal_colours = true # default => true
# Warn when jobs are not enqueued to Redis but to a job array
config.warn_when_jobs_not_processed_by_sidekiq = true # default => true
endenqueue_sidekiq_jobhave_enqueued_sidekiq_jobhave_jobhave_job_optionhave_job_optionsbe_processed_inbe_retryablesave_backtracebe_uniquebe_expired_inbe_delayed (deprecated)enqueue_sidekiq_jobDescribes that the block should enqueue a job. Optionally specify the specific job class, arguments, timing, and other context
…have_enqueued_sidekiq_jobDescribes that there should be an enqueued job (with the specified arguments):
AwesomeJob.perform_async 'Awesome', true
# test with...
expect(AwesomeJob).to have_enqueued_sidekiq_job
expect(AwesomeJob).to have_enqueued_sidekiq_job('Awesome', true)You can use the built-in RSpec args matchers too:
AwesomeJob.perform_async({"something" => "Awesome", "extra" => "stuff"})
# using built-in matchers from rspec-mocks:
expect(AwesomeJob).to have_enqueued_sidekiq_job(hash_including("something" => "Awesome"))
expect(AwesomeJob).to have_enqueued_sidekiq_job(any_args)
expect(AwesomeJob).to have_enqueued_sidekiq_job(hash_excluding("bad_stuff" => anything))
# composable as well
expect(AwesomeJob).to have_enqueued_sidekiq_job(any_args).and have_enqueued_sidekiq_job(hash_including("something" => "Awesome"))You can specify the number of jobs enqueued:
expect(AwesomeJob).to have_enqueued_sidekiq_job.once
expect(AwesomeJob).to have_enqueued_sidekiq_job.exactly(1).time
expect(AwesomeJob).to have_enqueued_sidekiq_job.exactly(:once)
expect(AwesomeJob).to have_enqueued_sidekiq_job.at_least(1).time
expect(AwesomeJob).to have_enqueued_sidekiq_job.at_least(:once)
expect(AwesomeJob).to have_enqueued_sidekiq_job.at_most(2).times
expect(AwesomeJob).to have_enqueued_sidekiq_job.at_most(:twice)
expect(AwesomeJob).to have_enqueued_sidekiq_job.at_most(:thrice)Likewise, specify what should be in the context:
AwesomeJob.set(trace_id: "something").perform_async
expect(AwesomeJob).to have_enqueued_sidekiq_job.with_context(trace_id: anything)Use chainable matchers #at, #in and #immediately
time = 5.minutes.from_now
AwesomeJob.perform_at time, 'Awesome', true
# test with...
expect(AwesomeJob).to have_enqueued_sidekiq_job('Awesome', true).at(time)AwesomeJob.perform_in 5.minutes, 'Awesome', true
# test with...
expect(AwesomeJob).to have_enqueued_sidekiq_job('Awesome', true).in(5.minutes)# Job scheduled for a date in the past are enqueued immediately.
AwesomeJob.perform_later 5.minutes.ago, 'Awesome', true # equivalent to: AwesomeJob.perform_async 'Awesome', true
# test with...
expect(AwesomeJob).to have_enqueued_sidekiq_job('Awesome', true).immediatelyUse the chainable #on matcher
class AwesomeJob
include Sidekiq::Job
sidekiq_options queue: :low
end
AwesomeJob.perform_async("a little awesome")
# test with..
expect(AwesomeJob).to have_enqueued_sidekiq_job("a little awesome").on("low")
# Setting the queue when enqueuing
AwesomeJob.set(queue: "high").perform_async("Very Awesome!")
expect(AwesomeJob).to have_enqueued_sidekiq_job("Very Awesome!").on("high")user = User.first
AwesomeActionMailer.invite(user, true).deliver_later
expect(Sidekiq::Worker).to have_enqueued_sidekiq_job(
"AwesomeActionMailer",
"invite",
"deliver_now",
user,
true
)have_jobDescribes that a Sidekiq set (ScheduledSet, RetrySet, DeadSet) should contain a job. Typically used together with stub_named_queues.
…expect(Sidekiq::RetrySet.new)
.to have_job(AwesomeJob)
.with('arg')
.with_error('something went wrong')
.with_error_class(RuntimeError)
.with_retry_count(2)expect(Sidekiq::DeadSet.new)
.to have_job(AwesomeJob)
.with('arg')
.died_within(1.hour)Use .scanning(pattern) to filter by a glob-style pattern matched against the job's JSON representation:
expect(Sidekiq::ScheduledSet.new).to have_job(AwesomeJob).scanning("*some_trace_id*")have_job_optionDescribes a single Sidekiq option set on a job class:
class AwesomeJob
include Sidekiq::Job
sidekiq_options retry: 5, queue: 'critical', dead: false
end
expect(AwesomeJob).to have_job_option(:retry, 5)
expect(AwesomeJob).to have_job_option(:queue, 'critical')
expect(AwesomeJob).to have_job_option(:dead, false)have_job_optionsDescribes multiple Sidekiq options set on a job class:
class AwesomeJob
include Sidekiq::Job
sidekiq_options retry: 5, queue: 'critical', backtrace: true
end
expect(AwesomeJob).to have_job_options(retry: 5, queue: 'critical', backtrace: true)be_processed_inDescribes the queue that a job should be processed in
sidekiq_options queue: :download
# test with...
expect(AwesomeJob).to be_processed_in :download # or
it { is_expected.to be_processed_in :download }be_retryableDescribes if a job should retry when there is a failure in its execution
Note: this only tests against the retry option in the job's Sidekiq options.
To test an enqueued job's retry, i.e. AwesomeJob.set(retry: 5), use
with_context
sidekiq_options retry: 5
# test with...
expect(AwesomeJob).to be_retryable true # or
it { is_expected.to be_retryable true }
# ...or alternatively specify the number of times it should be retried
expect(AwesomeJob).to be_retryable 5 # or
it { is_expected.to be_retryable 5 }
# ...or when it should not retry
expect(AwesomeJob).to be_retryable false # or
it { is_expected.to be_retryable false }save_backtraceDescribes if a job should save the error backtrace when there is a failure in its execution
sidekiq_options backtrace: 5
# test with...
expect(AwesomeJob).to save_backtrace # or
it { is_expected.to save_backtrace }
# ...or alternatively specify the number of lines that should be saved
expect(AwesomeJob).to save_backtrace 5 # or
it { is_expected.to save_backtrace 5 }
# ...or when it should not save the backtrace
expect(AwesomeJob).to_not save_backtrace # or
expect(AwesomeJob).to save_backtrace false # or
it { is_expected.to_not save_backtrace } # or
it { is_expected.to save_backtrace false }be_unique[!CAUTION] This is intended to for Sidekiq Enterprise unique job implementation. There is limited support for Sidekiq Unique Jobs, but compatibility is not guaranteed.
Describes when a job should be unique within its queue
sidekiq_options unique_for: 1.hour
# test with...
expect(AwesomeJob).to be_unique
it { is_expected.to be_unique }
# specify a specific interval
sidekiq_options unique_for: 1.hour
it { is_expected.to be_unique.for(1.hour) }until sub-matcher[!CAUTION] This sub-matcher only works for Sidekiq Enterprise
sidekiq_options unique_for: 1.hour, unique_until: :start
it { is_expected.to be_unique.until(:start) }be_expired_inDescribes when a job should expire
sidekiq_options expires_in: 1.hour
# test with...
it { is_expected.to be_expired_in 1.hour }
it { is_expected.to_not be_expired_in 2.hours }be_delayedThis matcher is deprecated. Use of it with Sidekiq 7+ will raise an error. Sidekiq 7 dropped Delayed Extensions.
Describes a method that should be invoked asynchronously (See [Sidekiq Delayed Extensions][sidekiq_wiki_delayed_extensions])
…require 'spec_helper'
describe AwesomeJob do
it { is_expected.to be_processed_in :my_queue }
it { is_expected.to be_retryable 5 }
it { is_expected.to be_unique }
it { is_expected.to be_expired_in 1.hour }
it 'enqueues another awesome job' do
subject.perform
expect(AnotherAwesomeJob).to have_enqueued_sidekiq_job('Awesome', true)
end
endstub_named_queuesIf you need to test jobs in Sidekiq's named sets (ScheduledSet, RetrySet, DeadSet) without
a Redis instance, opt-in with stub_named_queues: true. This replaces those sets with
in-memory implementations backed by a JobStore.
…If you are using Sidekiq Batches (Sidekiq Pro feature),
You can opt-in with stub_batches to make rspec-sidekiq mock the
implementation (using a NullObject pattern). This enables testing without a
Redis instance. Mocha and RSpec stubbing is supported here.
[!CAUTION] Opting-in to this feature, while allowing you to test without having Redis, does not provide the exact API that
Sidekiq::Batchdoes. As such it can cause surprises.
RSpec.describe "Using mocked batches", stub_batches: true do
it "uses mocked batches" do
batch = Sidekiq::Batch.new
batch.jobs do
SomeJob.perform_async 123
end
expect(SomeJob).to have_enqueued_sidekiq_job
# Caution, the NullObject pattern means that the mocked Batch implementation
# responds to anything... even if it's not on the true `Sidekiq::Batch` API
# For example, the following fails
expect { batch.foobar! }.to raise_error(NoMethodError)
end
endsidekiq_retries_exhausted do |msg|
bar('hello')
end
# test with...
FooClass.within_sidekiq_retries_exhausted_block {
expect(FooClass).to receive(:bar).with('hello')
}bundle exec rspecPlease do! If there's a feature missing that you'd love to see then get in on the action!
Issues/Pull Requests/Comments all welcome...
No open issues yet, or sync has not completed.