
Security News
Follow-up and Clarification on Recent Malicious Ruby Gems Campaign
A clarification on our recent research investigating 60 malicious Ruby gems.
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)enqueue_sidekiq_job
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).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
)
be_processed_in
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 }
be_retryable
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 }
save_backtrace
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 }
be_unique
:warning: 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:warning: 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_in
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 }
be_delayed
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.
:warning: 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
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
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...
FAQs
Unknown package
We found that rspec-sidekiq demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
A clarification on our recent research investigating 60 malicious Ruby gems.
Security News
ESLint now supports parallel linting with a new --concurrency flag, delivering major speed gains and closing a 10-year-old feature request.
Research
/Security News
A malicious Go module posing as an SSH brute forcer exfiltrates stolen credentials to a Telegram bot controlled by a Russian-speaking threat actor.