Skip to content

Support expected counts #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Unreleased
---
* [BREAKING] Make `have_enqueued_sidekiq_job()` match jobs with any arguments (same as `enqueue_sidekiq_job()` or `have_enqueued_sidekiq_job(any_args)`) ([@3v0k4](https://github.com/3v0k4) #215)
* [BREAKING] Make `have_enqueued_sidekiq_job()` match jobs with any arguments (same as `enqueue_sidekiq_job()` or `have_enqueued_sidekiq_job(any_args)`) (#215)
* Add support for expected number of jobs to both `enqueue_sidekiq_job` and `have_enqueued_sidekiq_job` (#219)

4.2.0
---
Expand Down
54 changes: 40 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ end
```

## Matchers
* [enqueue_sidekiq_job](#enqueue_sidekiq_job)
* [have_enqueued_sidekiq_job](#have_enqueued_sidekiq_job)
* [be_processed_in](#be_processed_in)
* [be_retryable](#be_retryable)
* [be_unique](#be_unique)
* [be_delayed (_deprecated_)](#be_delayed)
* [```enqueue_sidekiq_job```](#enqueue_sidekiq_job)
* [```have_enqueued_sidekiq_job```](#have_enqueued_sidekiq_job)
* [```be_processed_in```](#be_processed_in)
* [```be_retryable```](#be_retryable)
* [```save_backtrace```](#save_backtrace)
* [```be_unique```](#be_unique)
* [```be_expired_in```](#be_expired_in)
* [```be_delayed``` (_deprecated_)](#be_delayed)

### enqueue_sidekiq_job
### ```enqueue_sidekiq_job```

*Describes that the block should enqueue a job*. Optionally specify the
specific job class, arguments, timing, and other context
Expand All @@ -68,6 +70,17 @@ 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.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)

# Combine and chain them as desired
expect { AwesomeJob.perform_at(specific_time, "Awesome!") }.to(
enqueue_sidekiq_job(AwesomeJob)
Expand All @@ -83,7 +96,7 @@ expect do
end.to enqueue_sidekiq_job(AwesomeJob).and enqueue_sidekiq_job(OtherJob)
```

### have_enqueued_sidekiq_job
### ```have_enqueued_sidekiq_job```

Describes that there should be an enqueued job (with the specified arguments):

Expand All @@ -107,6 +120,19 @@ expect(AwesomeJob).to have_enqueued_sidekiq_job(hash_excluding("bad_stuff" => an
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:

```ruby
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)
```

#### Testing scheduled jobs

*Use chainable matchers `#at`, `#in` and `#immediately`*
Expand Down Expand Up @@ -167,7 +193,7 @@ expect(Sidekiq::Worker).to have_enqueued_sidekiq_job(
)
```

### be_processed_in
### ```be_processed_in```
*Describes the queue that a job should be processed in*
```ruby
sidekiq_options queue: :download
Expand All @@ -176,7 +202,7 @@ expect(AwesomeJob).to be_processed_in :download # or
it { is_expected.to be_processed_in :download }
```

### be_retryable
### ```be_retryable```
*Describes if a job should retry when there is a failure in its execution*
```ruby
sidekiq_options retry: 5
Expand All @@ -191,7 +217,7 @@ expect(AwesomeJob).to be_retryable false # or
it { is_expected.to be_retryable false }
```

### save_backtrace
### ```save_backtrace```
*Describes if a job should save the error backtrace when there is a failure in its execution*
```ruby
sidekiq_options backtrace: 5
Expand All @@ -208,7 +234,7 @@ it { is_expected.to_not save_backtrace } # or
it { is_expected.to save_backtrace false }
```

### be_unique
### ```be_unique```
*Describes when a job should be unique within its queue*
```ruby
sidekiq_options unique: true
Expand All @@ -217,7 +243,7 @@ expect(AwesomeJob).to be_unique
it { is_expected.to be_unique }
```

### be_expired_in
### ```be_expired_in```
*Describes when a job should expire*
```ruby
sidekiq_options expires_in: 1.hour
Expand All @@ -226,7 +252,7 @@ it { is_expected.to be_expired_in 1.hour }
it { is_expected.to_not be_expired_in 2.hours }
```

### be_delayed
### ```be_delayed```

**This matcher is deprecated**. Use of it with Sidekiq 7+ will raise an error.
Sidekiq 7 [dropped Delayed
Expand Down
94 changes: 87 additions & 7 deletions lib/rspec/sidekiq/matchers/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,19 @@ def initialize(klass)
@jobs = unwrap_jobs(klass.jobs).map { |job| EnqueuedJob.new(job) }
end

def includes?(arguments, options)
!!jobs.find { |job| matches?(job, arguments, options) }
def includes?(arguments, options, count)
matching = jobs.filter { |job| matches?(job, arguments, options) }

case count
in [:exactly, n]
matching.size == n
in [:at_least, n]
matching.size >= n
in [:at_most, n]
matching.size <= n
else
matching.size > 0
end
end

def each(&block)
Expand Down Expand Up @@ -164,11 +175,12 @@ class Base
include RSpec::Mocks::ArgumentMatchers
include RSpec::Matchers::Composable

attr_reader :expected_arguments, :expected_options, :klass, :actual_jobs
attr_reader :expected_arguments, :expected_options, :klass, :actual_jobs, :expected_count

def initialize
@expected_arguments = [any_args]
@expected_options = {}
set_expected_count :positive, 1
end

def with(*expected_arguments)
Expand Down Expand Up @@ -196,12 +208,59 @@ def on(queue)
self
end

def once
set_expected_count :exactly, 1
self
end

def twice
set_expected_count :exactly, 2
self
end

def thrice
set_expected_count :exactly, 3
self
end

def exactly(n)
set_expected_count :exactly, n
self
end

def at_least(n)
set_expected_count :at_least, n
self
end

def at_most(n)
set_expected_count :at_most, n
self
end

def times
self
end
alias :time :times

def set_expected_count(relativity, n)
n =
case n
when Integer then n
when :once then 1
when :twice then 2
when :thrice then 3
else raise ArgumentError, "Unsupported #{n} in '#{relativity} #{n}'. Use either an Integer, :once, :twice, or :thrice."
end
@expected_count = [relativity, n]
end

def description
"have an enqueued #{klass} job with arguments #{expected_arguments}"
"#{common_message} with arguments #{expected_arguments}"
end

def failure_message
message = ["expected to have an enqueued #{klass} job"]
message = ["expected to #{common_message}"]
if expected_arguments
message << " with arguments:"
message << " -#{formatted(expected_arguments)}"
Expand All @@ -213,7 +272,7 @@ def failure_message
end

if actual_jobs.any?
message << "but have enqueued only jobs"
message << "but enqueued only jobs"
if expected_arguments
job_messages = actual_jobs.map do |job|
base = " -JID:#{job.jid} with arguments:"
Expand All @@ -227,13 +286,34 @@ def failure_message

message << job_messages.join("\n")
end
else
message << "but enqueued 0 jobs"
end

message.join("\n")
end

def common_message
"#{prefix_message} #{count_message} #{klass} #{expected_count.last == 1 ? "job" : "jobs"}"
end

def prefix_message
raise NotImplementedError
end

def count_message
case expected_count
in [:positive, _]
"a"
in [:exactly, n]
n
in [relativity, n]
"#{relativity.to_s.gsub('_', ' ')} #{n}"
end
end

def failure_message_when_negated
message = ["expected not to have an enqueued #{klass} job"]
message = ["expected not to #{common_message} but enqueued #{actual_jobs.count}"]
message << " arguments: #{expected_arguments}" if expected_arguments.any?
message << " options: #{expected_options}" if expected_options.any?
message.join("\n")
Expand Down
19 changes: 3 additions & 16 deletions lib/rspec/sidekiq/matchers/enqueue_sidekiq_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,11 @@ def matches?(proc)
return false
end

@actual_jobs.includes?(expected_arguments, expected_options)
@actual_jobs.includes?(expected_arguments, expected_options, expected_count)
end

def failure_message
if @actual_jobs.none?
"expected to enqueue a job but enqueued 0"
else
super
end
end

def failure_message_when_negated
messages = ["expected not to enqueue a #{@klass} job but enqueued #{actual_jobs.count}"]

messages << " with arguments #{formatted(expected_arguments)}" if expected_arguments
messages << " with context #{formatted(expected_options)}" if expected_options

messages.join("\n")
def prefix_message
"enqueue"
end

def supports_block_expectations?
Expand Down
7 changes: 6 additions & 1 deletion lib/rspec/sidekiq/matchers/have_enqueued_sidekiq_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ def matches?(job_class)

actual_jobs.includes?(
expected_arguments == [] ? any_args : expected_arguments,
expected_options
expected_options,
expected_count
)
end

def prefix_message
"have enqueued"
end
end
end
end
Expand Down
Loading