Welcome @wspurgin as new maintainer for rspec-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'
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
- be_unique
- 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
# 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)
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('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"))
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).immediately
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")
user = User.first
AwesomeActionMailer.invite(user, true).deliver_later
expect(Sidekiq::Worker).to have_enqueued_sidekiq_job(
"AwesomeActionMailer",
"invite",
"deliver_now",
user,
true
)
The negative case for have_enqueued_sidekiq_job
is provided, but it's
important to remember that have_enqueued_sidekiq_job
is an expectation that a
job is enqueued with specific arguments. In other words, passing no arguments
to have_enqueued_sidekiq_job
is implicitly telling the matcher to look for
jobs without arguments.
In short, unless you tell the matcher that no jobs with any arguments should be enqueued, you'll get the wrong result:
# example this is a test that we'd expect to fail
AwesomeJob.perform_async "Actually not awesome"
### BAD - saying there shouldn't be a job enqueued _without_ args
expect(AwesomeJob).not_to have_enqueued_sidekiq_job
# => passes! 😱 Our job was enqueued _with_ args so no job exists without args.
### Good
expect(AwesomeJob).not_to have_enqueued_sidekiq_job(any_args)
# => fails
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
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 }
Describes when a job should be unique within its queue
sidekiq_options unique: true
# test with...
expect(AwesomeJob).to be_unique
it { is_expected.to be_unique }
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.
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
end
sidekiq_retries_exhausted do |msg|
bar('hello')
end
# test with...
FooClass.within_sidekiq_retries_exhausted_block {
expect(FooClass).to receive(:bar).with('hello')
}
bundle exec rspec spec
- @wspurgin
- @packrat386
- @philostler
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...