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' end
rspec-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 job
array to enable testing (see the FAQ & Troubleshooting Wiki page). Thus, only include gem "rspec-sidekiq"
in environments where this behaviour is required, such as the test
group.
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 end
enqueue_sidekiq_job
have_enqueued_sidekiq_job
be_processed_in
be_retryable
save_backtrace
be_unique
be_expired_in
be_delayed
(deprecated)Describes that the block should enqueue a job. Optionally specify the specific job class, arguments, timing, and other context
# Basic expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job # A specific job class expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job(AwesomeJob) # with specific arguments expect { AwesomeJob.perform_async "Awesome!" }.to enqueue_sidekiq_job.with("Awesome!") # On a specific queue expect { AwesomeJob.set(queue: "high").perform_async }.to enqueue_sidekiq_job.on("high") # At a specific datetime specific_time = 1.hour.from_now expect { AwesomeJob.perform_at(specific_time) }.to enqueue_sidekiq_job.at(specific_time) # In a specific interval (be mindful of freezing or managing time here) freeze_time do expect { AwesomeJob.perform_in(1.hour) }.to enqueue_sidekiq_job.in(1.hour) end # A specific number of times expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.never expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.exactly(0) expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.exactly(0).time expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.once expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.exactly(1).time expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.exactly(:once) expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.at_least(1).time expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.at_least(:once) expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.at_most(2).times expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.at_most(:twice) expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.at_most(:thrice) # With specific context: # Useful for testing anything `set` on the job, including # overrides to things like `retry` expect { AwesomeJob.set(trace_id: "something").perform_async }.to enqueue_sidekiq_job.with_context(trace_id: anything) expect { AwesomeJob.set(retry: 5).perform_async }.to enqueue_sidekiq_job.with_context(retry: 5) # Combine and chain them as desired expect { AwesomeJob.perform_at(specific_time, "Awesome!") }.to( enqueue_sidekiq_job(AwesomeJob) .with("Awesome!") .on("default") .at(specific_time) ) # Also composable expect do AwesomeJob.perform_async OtherJob.perform_async end.to enqueue_sidekiq_job(AwesomeJob).and enqueue_sidekiq_job(OtherJob)
have_enqueued_sidekiq_job
Describes 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).immediatelyTesting queue set for job
Use 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")Testing ActiveMailer jobs
user = User.first AwesomeActionMailer.invite(user, true).deliver_later expect(Sidekiq::Worker).to have_enqueued_sidekiq_job( "AwesomeActionMailer", "invite", "deliver_now", user, true )
Describes 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 }
Describes 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 }
Describes 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 }
⚠️ 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) }
⚠️ 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) }
Describes 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 }
This 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)
Object.delay.is_nil? # delay expect(Object.method :is_nil?).to be_delayed Object.delay.is_a? Object # delay with argument expect(Object.method :is_a?).to be_delayed(Object) Object.delay_for(1.hour).is_nil? # delay for expect(Object.method :is_nil?).to be_delayed.for 1.hour Object.delay_for(1.hour).is_a? Object # delay for with argument expect(Object.method :is_a?).to be_delayed(Object).for 1.hour Object.delay_until(1.hour.from_now).is_nil? # delay until expect(Object.method :is_nil?).to be_delayed.until 1.hour.from_now Object.delay_until(1.hour.from_now).is_a? Object # delay until with argument expect(Object.method :is_a?).to be_delayed(Object).until 1.hour.from_now #Rails Mailer MyMailer.delay.some_mail expect(MyMailer.instance_method :some_mail).to be_delayed
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 end
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::Batch
does. 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 endwithin_sidekiq_retries_exhausted_block
sidekiq_retries_exhausted do |msg| bar('hello') end # test with... FooClass.within_sidekiq_retries_exhausted_block { expect(FooClass).to receive(:bar).with('hello') }
Please 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...
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4