A test double of Resque for RSpec and Cucumber. The code was originally based on http://github.com/justinweiss/resque_unit.
ResqueSpec will also fire Resque hooks if you are using them. See below.
Update your Gemfile to include resque_spec
only in the test group (Not
using bundler
? Do the necessary thing for your app's gem management and use
bundler
. resque_spec
monkey patches resque
it should only be used with
your tests!)
group :test do
gem 'resque_spec'
end
By default, the above will add the ResqueSpec
module and make it available in
Cucumber. If you want the with_resque
and without_resque
helpers, manually
require the resque_spec/cucumber
module:
require 'resque_spec/cucumber'
This can be done in features/support/env.rb
or in a specific support file
such as features/support/resque.rb
.
ResqueSpec implements the stable API for Resque 1.19+ (which is enqueue
,
enqueue_to
, dequeue
, peek
, reserve
, size
, the Resque hooks, and
because of the way resque_scheduler
works Job.create
and Job.destroy
).
It does not have a test double for Redis, so this may lead to some interesting and
puzzling behaviour if you use some of the popular Resque plugins (such as
resque_lock
).
Given this scenario
Given a person
When I recalculate
Then the person has calculate queued
And I write this spec using the resque_spec
matcher
describe "#recalculate" do
before do
ResqueSpec.reset!
end
it "adds person.calculate to the Person queue" do
person.recalculate
expect(Person).to have_queued(person.id, :calculate)
end
end
And I see that the have_queued
assertion is asserting that the Person
queue has a job with arguments person.id
and :calculate
And I take note of the before
block that is calling reset!
for every spec
And I might use the in
statement to specify the queue:
describe "#recalculate" do
before do
ResqueSpec.reset!
end
it "adds person.calculate to the Person queue" do
person.recalculate
expect(Person).to have_queued(person.id, :calculate).in(:people)
end
end
And I might write this as a Cucumber step
Then /the (\w?) has (\w?) queued/ do |thing, method|
thing_obj = instance_variable_get("@#{thing}")
expect(thing_obj.class).to have_queued(thing_obj.id, method.to_sym)
end
Then I write some code to make it pass:
class Person
@queue = :people
def recalculate
Resque.enqueue(Person, id, :calculate)
end
end
You can check the size of the queue in your specs too.
describe "#recalculate" do
before do
ResqueSpec.reset!
end
it "adds an entry to the Person queue" do
person.recalculate
expect(Person).to have_queue_size_of(1)
end
end
Occasionally, you want to run your specs directly against Resque instead of
ResqueSpec. For one at a time use, pass a block to the without_resque_spec
helper:
describe "#recalculate" do
it "recalculates the persons score" do
without_resque_spec do
person.recalculate
end
... assert recalculation after job done
end
end
Or you can manage when ResqueSpec is disabled by flipping the
ResqueSpec.disable_ext
flag:
# disable ResqueSpec
ResqueSpec.disable_ext = true
You will most likely (but not always, see the Resque docs) need to ensure that
you have redis
running.
To use with ResqueMailer you should
have an initializer that does not exclude the test
(or cucumber
)
environment. Your initializer will probably end up looking like:
# config/initializers/resque_mailer.rb
Resque::Mailer.excluded_environments = []
If you have a mailer like this:
class ExampleMailer < ActionMailer::Base
include Resque::Mailer
def welcome_email(user_id)
end
end
You can write a spec like this:
describe "#welcome_email" do
before do
ResqueSpec.reset!
Examplemailer.welcome_email(user.id).deliver
end
subject { described_class }
it { should have_queue_size_of(1) }
it { should have_queued(:welcome_email, [user.id]) }
end
Update the Gemfile to enable the resque-schedular
matchers:
group :test do
gem 'resque_spec', require: 'resque_spec/scheduler'
end
Given this scenario
Given a person
When I schedule a recalculate
Then the person has calculate scheduled
And I write this spec using the resque_spec
matcher
describe "#recalculate" do
before do
ResqueSpec.reset!
end
it "adds person.calculate to the Person queue" do
person.recalculate
expect(Person).to have_scheduled(person.id, :calculate)
end
end
And I might use the at
statement to specify the time:
describe "#recalculate" do
before do
ResqueSpec.reset!
end
it "adds person.calculate to the Person queue" do
person.recalculate
# Is it scheduled to be executed at 2010-02-14 06:00:00 ?
expect(Person).to have_scheduled(person.id, :calculate).at(Time.mktime(2010,2,14,6,0,0))
end
end
And I might use the in
statement to specify time interval (in seconds):
describe "#recalculate" do
before do
ResqueSpec.reset!
end
it "adds person.calculate to the Person queue" do
person.recalculate
# Is it scheduled to be executed in 5 minutes?
expect(Person).to have_scheduled(person.id, :calculate).in(5 * 60)
end
end
You can also check the size of the schedule:
describe "#recalculate" do
before do
ResqueSpec.reset!
end
it "adds person.calculate to the Person queue" do
person.recalculate
expect(Person).to have_schedule_size_of(1)
end
end
(And I take note of the before
block that is calling reset!
for every spec)
You can explicitly specify the queue when using enqueue_at_with_queue and enqueue_in_with_queue:
describe "#recalculate_in_future" do
before do
ResqueSpec.reset!
end
it "adds person.calculate to the :future queue" do
person.recalculate_in_future
Person.should have_schedule_size_of(1).queue(:future)
end
end
And I might write this as a Cucumber step
Then /the (\w?) has (\w?) scheduled/ do |thing, method|
thing_obj = instance_variable_get("@#{thing}")
expect(thing_obj.class).to have_scheduled(thing_obj.id, method.to_sym)
end
Then I write some code to make it pass:
class Person
@queue = :people
def recalculate
Resque.enqueue_at(Time.now + 3600, Person, id, :calculate)
end
def recalculate_in_future
Resque.enqueue_at_with_queue(:future, Time.now + 3600, Person, id, :calculate)
end
end
Normally, ResqueSpec does not perform queued jobs within tests. You may want to make assertions based on the result of your jobs. ResqueSpec can process jobs immediately as they are queued or under your control.
To perform jobs immediately, you can pass a block to the with_resque
helper:
Given this scenario
Given a game
When I score
Then the game has a score
I might write this as a Cucumber step
When /I score/ do
with_resque do
visit game_path
click_link 'Score!'
end
end
Or I write this spec using the with_resque
helper
describe "#score!" do
before do
ResqueSpec.reset!
end
it "increases the score" do
with_resque do
game.score!
end
expect(game.score).to == 10
end
end
You can turn this behavior on by setting ResqueSpec.inline = true
.
You can perform the first job on a queue at a time, or perform all the jobs on
a queue. Use ResqueSpec#perform_next(queue_name)
or
ResqueSpec#perform_all(queue_name)
Given this scenario:
Given a game
When I score
And the score queue runs
Then the game has a score
I might write this as a Cucumber step
When /the (\w+) queue runs/ do |queue_name|
ResqueSpec.perform_all(queue_name)
end
Resque provides hooks at different points of the queueing lifecylce. ResqueSpec fires these hooks when appropriate.
The before and after enqueue
hooks are always called when you use
Resque#enqueue
. If your before_enqueue
hook returns false
, the job will
not be queued and after_enqueue
will not be called.
The perform
hooks: before, around, after, and on failure are fired by
ResqueSpec if you are using the with_resque
helper or set ResqueSpec.inline = true
.
Important! If you are using resque-scheduler, Resque#enqueue_at/enqueue_in
does not fire the after enqueue hook (the job has not been queued yet!), but
will fire the perform
hooks if you are using inline
mode.
I made resque_spec
because resque is awesome and should be easy to spec.
Follow me on Github and
Twitter.
have_scheduled
times
chained matcherhave_queue_size_of_at_least
and have_schedule_size_of_at_least
matchersrspec
dependency with explicit dependenciesbe_queued
matcherhave_scheduled
matcherCopyright (c) 2010-2015 Les Hill. See LICENSE for details.