Skip to content

Commit

Permalink
Support expected counts (#219)
Browse files Browse the repository at this point in the history
* support expected counts

* do not allow floats

* better naming

* remove unused code

* only include pr number for consistency

* fail fast if n cannot be coerced
  • Loading branch information
3v0k4 committed Apr 25, 2024
1 parent 8066f7c commit 865e45c
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 57 deletions.
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

0 comments on commit 865e45c

Please sign in to comment.