Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add enqueue_sidekiq_job block matcher
Fixes #170
- Loading branch information
Showing
6 changed files
with
350 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
module RSpec | ||
module Sidekiq | ||
module Matchers | ||
def enqueue_sidekiq_job(worker_class) | ||
EnqueuedSidekiqJob.new(worker_class) | ||
end | ||
|
||
class EnqueuedSidekiqJob | ||
include RSpec::Matchers::Composable | ||
|
||
def initialize(worker_class) | ||
@worker_class = worker_class | ||
end | ||
|
||
def with(*expected_arguments, **kwargs) | ||
fail 'keyword arguments serialization is not supported by Sidekiq' unless kwargs.empty? | ||
|
||
@expected_arguments = expected_arguments | ||
self | ||
end | ||
|
||
def at(timestamp) | ||
fail 'setting expecations with both `at` and `in` is not supported' if @expected_in | ||
|
||
@expected_at = timestamp | ||
self | ||
end | ||
|
||
def in(interval) | ||
fail 'setting expecations with both `at` and `in` is not supported' if @expected_at | ||
|
||
@expected_in = interval | ||
self | ||
end | ||
|
||
def matches?(block) | ||
filter(enqueued_in_block(block)).one? | ||
end | ||
|
||
def does_not_match?(block) | ||
filter(enqueued_in_block(block)).none? | ||
end | ||
|
||
def failure_message | ||
message = ["expected to enqueue #{worker_class} job"] | ||
message << " arguments: #{expected_arguments}" if expected_arguments | ||
message << " in: #{expected_in.inspect}" if expected_in | ||
message << " at: #{expected_at}" if expected_at | ||
message.join("\n") | ||
end | ||
|
||
def failure_message_when_negated | ||
message = ["expected not to enqueue #{worker_class} job"] | ||
message << " arguments: #{expected_arguments}" if expected_arguments | ||
message << " in: #{expected_in.inspect}" if expected_in | ||
message << " at: #{expected_at}" if expected_at | ||
message.join("\n") | ||
end | ||
|
||
def supports_block_expectations? | ||
true | ||
end | ||
|
||
def supports_value_expectations? | ||
false | ||
end | ||
|
||
private | ||
|
||
def enqueued_in_block(block) | ||
before = @worker_class.jobs.dup | ||
block.call | ||
@worker_class.jobs - before | ||
end | ||
|
||
def filter(jobs) | ||
jobs = jobs.select { |job| timestamps_match_rounded_to_seconds?(expected_at, job['at']) } if expected_at | ||
jobs = jobs.select { |job| intervals_match_rounded_to_seconds?(expected_in, job['at']) } if expected_in | ||
jobs = jobs.select { |job| values_match?(expected_arguments, job['args']) } if expected_arguments | ||
jobs | ||
end | ||
|
||
# Due to zero nsec precision of `Time.now` (and therefore `5.minutes.from_now`) on | ||
# some platforms, and lossy Sidekiq serialization that uses `.to_f` on timestamps, | ||
# values won't match unless rounded. | ||
# Rounding to whole seconds is sub-optimal but simple. | ||
def timestamps_match_rounded_to_seconds?(expected, actual) | ||
return false if actual.nil? | ||
|
||
actual_time = Time.at(actual) | ||
values_match?(expected_at, actual_time) || | ||
expected_at.to_i == actual_time.to_i | ||
end | ||
|
||
def intervals_match_rounded_to_seconds?(expected, actual) | ||
return false if actual.nil? | ||
|
||
actual_time = Time.at(actual) | ||
expected_in.from_now.to_i == actual_time.to_i | ||
end | ||
|
||
attr_reader :worker_class, :expected_arguments, :expected_at, :expected_in | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
203 changes: 203 additions & 0 deletions
203
spec/rspec/sidekiq/matchers/enqueue_sidekiq_job_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
require 'spec_helper' | ||
|
||
RSpec.describe RSpec::Sidekiq::Matchers::EnqueuedSidekiqJob do | ||
let(:worker) do | ||
Class.new do | ||
include ::Sidekiq::Worker | ||
end | ||
end | ||
|
||
let(:another_worker) do | ||
Class.new do | ||
include ::Sidekiq::Worker | ||
end | ||
end | ||
|
||
it 'raises ArgumentError when used in value expectation' do | ||
expect { | ||
expect(worker.perform_async).to enqueue_sidekiq_job(worker) | ||
}.to raise_error | ||
end | ||
|
||
it 'fails when no worker class is specified' do | ||
expect { | ||
expect { worker.perform_async }.to enqueue_sidekiq_job | ||
}.to raise_error(ArgumentError) | ||
end | ||
|
||
it 'passes' do | ||
expect { worker.perform_async } | ||
.to enqueue_sidekiq_job(worker) | ||
end | ||
|
||
it 'fails when negated and job is enqueued' do | ||
expect { | ||
expect { worker.perform_async }.not_to enqueue_sidekiq_job(worker) | ||
}.to raise_error(/expected not to enqueue/) | ||
end | ||
|
||
context 'when no jobs were enqueued' do | ||
it 'fails' do | ||
expect { | ||
expect { } # nop | ||
.to enqueue_sidekiq_job(worker) | ||
}.to raise_error(/expected to enqueue/) | ||
end | ||
|
||
it 'passes with negation' do | ||
expect { } # nop | ||
.not_to enqueue_sidekiq_job(worker) | ||
end | ||
end | ||
|
||
context 'with another worker' do | ||
it 'fails' do | ||
expect { | ||
expect { worker.perform_async } | ||
.to enqueue_sidekiq_job(another_worker) | ||
}.to raise_error(/expected to enqueue/) | ||
end | ||
|
||
it 'passes with negation' do | ||
expect { worker.perform_async } | ||
.not_to enqueue_sidekiq_job(another_worker) | ||
end | ||
end | ||
|
||
it 'counts only jobs enqueued in block' do | ||
worker.perform_async | ||
expect { }.not_to enqueue_sidekiq_job(worker) | ||
end | ||
|
||
it 'counts jobs enqueued in block' do | ||
worker.perform_async | ||
expect { worker.perform_async }.to enqueue_sidekiq_job(worker) | ||
end | ||
|
||
it 'fails when too many jobs enqueued' do | ||
expect { | ||
expect { | ||
worker.perform_async | ||
worker.perform_async | ||
}.to enqueue_sidekiq_job(worker) | ||
}.to raise_error(/expected to enqueue/) | ||
end | ||
|
||
it 'fails when negated and several jobs enqueued' do | ||
expect { | ||
expect { | ||
worker.perform_async | ||
worker.perform_async | ||
}.not_to enqueue_sidekiq_job(worker) | ||
}.to raise_error(/expected not to enqueue/) | ||
end | ||
|
||
it 'passes with multiple jobs' do | ||
expect { | ||
another_worker.perform_async | ||
worker.perform_async | ||
} | ||
.to enqueue_sidekiq_job(worker) | ||
.and enqueue_sidekiq_job(another_worker) | ||
end | ||
|
||
context 'when enqueued with perform_at' do | ||
it 'passes' do | ||
future = 1.minute.from_now | ||
expect { worker.perform_at(future) } | ||
.to enqueue_sidekiq_job(worker).at(future) | ||
end | ||
|
||
it 'fails when timestamps do not match' do | ||
future = 1.minute.from_now | ||
expect { | ||
expect { worker.perform_at(future) } | ||
.to enqueue_sidekiq_job(worker).at(2.minutes.from_now) | ||
}.to raise_error(/expected to enqueue.+at:/m) | ||
end | ||
|
||
it 'matches timestamps with nanosecond precision' do | ||
100.times do | ||
future = 1.minute.from_now | ||
future = future.change(nsec: future.nsec.round(-3) + rand(999)) | ||
expect { worker.perform_at(future) } | ||
.to enqueue_sidekiq_job(worker).at(future) | ||
end | ||
end | ||
|
||
it 'accepts composable matchers' do | ||
future = 1.minute.from_now | ||
slightly_earlier = 58.seconds.from_now | ||
expect { worker.perform_at(slightly_earlier) } | ||
.to enqueue_sidekiq_job(worker).at(a_value_within(5.seconds).of(future)) | ||
end | ||
|
||
it 'fails when the job was enuqued for now' do | ||
expect { | ||
expect { worker.perform_async } | ||
.to enqueue_sidekiq_job(worker).at(1.minute.from_now) | ||
}.to raise_error(/expected to enqueue.+at:/m) | ||
end | ||
end | ||
|
||
context 'when enqueued with perform_in' do | ||
it 'passes' do | ||
interval = 1.minute | ||
expect { worker.perform_in(interval) } | ||
.to enqueue_sidekiq_job(worker).in(interval) | ||
end | ||
|
||
it 'fails when timestamps do not match' do | ||
interval = 1.minute | ||
expect { | ||
expect { worker.perform_in(interval) } | ||
.to enqueue_sidekiq_job(worker).in(2.minutes) | ||
}.to raise_error(/expected to enqueue.+in:/m) | ||
end | ||
|
||
it 'fails when the job was enuqued for now' do | ||
expect { | ||
expect { worker.perform_async } | ||
.to enqueue_sidekiq_job(worker).in(1.minute) | ||
}.to raise_error(/expected to enqueue.+in:/m) | ||
end | ||
end | ||
|
||
it 'matches when not specified at and scheduled for the future' do | ||
expect { worker.perform_in(1.day) } | ||
.to enqueue_sidekiq_job(worker) | ||
expect { worker.perform_at(1.day.from_now) } | ||
.to enqueue_sidekiq_job(worker) | ||
end | ||
|
||
context 'with arguments' do | ||
it 'fails with kwargs' do | ||
expect { | ||
expect { worker.perform_async } | ||
.to enqueue_sidekiq_job(worker).with(42, name: 'David') | ||
}.to raise_error(/keyword arguments serialization is not supported by Sidekiq/) | ||
end | ||
|
||
it 'passes with provided arguments' do | ||
expect { worker.perform_async(42, 'David') } | ||
.to enqueue_sidekiq_job(worker).with(42, 'David') | ||
end | ||
|
||
it 'supports provided argument matchers' do | ||
expect { worker.perform_async(42, 'David') } | ||
.to enqueue_sidekiq_job(worker).with(be > 41, a_string_including('Dav')) | ||
end | ||
|
||
it 'passes when negated and arguments do not match' do | ||
expect { worker.perform_async(42, 'David') } | ||
.not_to enqueue_sidekiq_job(worker).with(11, 'Phil') | ||
end | ||
|
||
it 'fails when arguments do not match' do | ||
expect { | ||
expect { worker.perform_async(42, 'David') } | ||
.to enqueue_sidekiq_job(worker).with(11, 'Phil') | ||
}.to raise_error(/expected to enqueue.+arguments:/m) | ||
end | ||
end | ||
end |