diff --git a/features/job_specs/job_spec.feature b/features/job_specs/job_spec.feature index 660ff3326d..5be2e2f264 100644 --- a/features/job_specs/job_spec.feature +++ b/features/job_specs/job_spec.feature @@ -15,9 +15,11 @@ Feature: job spec * specify the queue which the job was enqueued to Check the documentation on - [`have_been_enqueued`](matchers/have-been-enqueued-matcher) and - [`have_enqueued_job`](matchers/have-enqueued-job-matcher) for more - information. + [`have_been_enqueued`](matchers/have-been-enqueued-matcher), + [`have_enqueued_job`](matchers/have-enqueued-job-matcher), + [`have_been_performed`](matchers/have-been-performed-matcher), and + [`have_performed_job`](matchers/have-performed-job-matcher) + for more information. Background: Given active job is available diff --git a/features/matchers/have_been_performed_matcher.feature b/features/matchers/have_been_performed_matcher.feature new file mode 100644 index 0000000000..59f4229d4b --- /dev/null +++ b/features/matchers/have_been_performed_matcher.feature @@ -0,0 +1,76 @@ +Feature: have_been_performed matcher + + The `have_been_performed` matcher is used to check if given ActiveJob job was performed. + + Background: + Given active job is available + + Scenario: Checking job class name + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adapter.perform_performed_jobs = true + UploadBackupsJob.perform_later + expect(UploadBackupsJob).to have_been_performed + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass + + Scenario: Checking passed arguments to job + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adapter.perform_performed_jobs = true + UploadBackupsJob.perform_later("users-backup.txt", "products-backup.txt") + expect(UploadBackupsJob).to( + have_been_performed.with("users-backup.txt", "products-backup.txt") + ) + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass + + Scenario: Checking job performed time + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adapter.perform_performed_jobs = true + UploadBackupsJob.set(:wait_until => Date.tomorrow.noon).perform_later + expect(UploadBackupsJob).to have_been_performed.at(Date.tomorrow.noon) + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass + + Scenario: Checking job queue name + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adapter.perform_performed_jobs = true + UploadBackupsJob.perform_later + expect(UploadBackupsJob).to have_been_performed.on_queue("default") + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass diff --git a/features/matchers/have_performed_job_matcher.feature b/features/matchers/have_performed_job_matcher.feature new file mode 100644 index 0000000000..6135d7ab26 --- /dev/null +++ b/features/matchers/have_performed_job_matcher.feature @@ -0,0 +1,96 @@ +Feature: have_performed_job matcher + + The `have_performed_job` (also aliased as `enqueue_job`) matcher is used to check if given ActiveJob job was performed. + + Background: + Given active job is available + + Scenario: Checking job class name + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adatper.perform_enqueued_jobs = true + expect { + UploadBackupsJob.perform_later + }.to have_performed_job(UploadBackupsJob) + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass + + Scenario: Checking passed arguments to job + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adatper.perform_enqueued_jobs = true + expect { + UploadBackupsJob.perform_later("users-backup.txt", "products-backup.txt") + }.to have_performed_job.with("users-backup.txt", "products-backup.txt") + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass + + Scenario: Checking job performed time + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adatper.perform_enqueued_jobs = true + expect { + UploadBackupsJob.set(:wait_until => Date.tomorrow.noon).perform_later + }.to have_performed_job.at(Date.tomorrow.noon) + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass + + Scenario: Checking job queue name + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adatper.perform_enqueued_jobs = true + expect { + UploadBackupsJob.perform_later + }.to have_performed_job.on_queue("default") + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass + + Scenario: Using alias method + Given a file named "spec/jobs/upload_backups_job_spec.rb" with: + """ruby + require "rails_helper" + + RSpec.describe UploadBackupsJob do + it "matches with performed job" do + ActiveJob::Base.queue_adapter = :test + ActiveJob::Base.queue_adatper.perform_enqueued_jobs = true + expect { + UploadBackupsJob.perform_later + }.to enqueue_job(UploadBackupsJob) + end + end + """ + When I run `rspec spec/jobs/upload_backups_job_spec.rb` + Then the examples should all pass diff --git a/lib/rspec/rails/matchers/active_job.rb b/lib/rspec/rails/matchers/active_job.rb index 52df3dfd1c..eeaa45dcf7 100644 --- a/lib/rspec/rails/matchers/active_job.rb +++ b/lib/rspec/rails/matchers/active_job.rb @@ -11,7 +11,9 @@ module ActiveJob # rubocop: disable Style/ClassLength # @private class Base < RSpec::Matchers::BuiltIn::BaseMatcher - def initialize + def initialize(verb_present_tense, verb_past_tense) + @verb_present_tense = verb_present_tense + @verb_past_tense = verb_past_tense @args = [] @queue = nil @at = nil @@ -67,7 +69,7 @@ def thrice end def failure_message - "expected to enqueue #{base_message}".tap do |msg| + "expected to #{@verb_present_tense} #{base_message}".tap do |msg| if @unmatching_jobs.any? msg << "\nQueued jobs:" @unmatching_jobs.each do |job| @@ -78,7 +80,7 @@ def failure_message end def failure_message_when_negated - "expected not to enqueue #{base_message}" + "expected not to #{@verb_present_tense} #{base_message}" end def message_expectation_modifier @@ -119,7 +121,7 @@ def base_message msg << " with #{@args}," if @args.any? msg << " on queue #{@queue}," if @queue msg << " at #{@at}," if @at - msg << " but enqueued #{@matching_jobs_count}" + msg << " but #{@verb_past_tense} #{@matching_jobs_count}" end end @@ -174,7 +176,7 @@ def queue_adapter # @private class HaveEnqueuedJob < Base def initialize(job) - super() + super("enqueue", "enqueued") @job = job end @@ -191,11 +193,45 @@ def matches?(proc) # @private class HaveBeenEnqueued < Base + def initialize + super("enqueue", "enqueued") + end + def matches?(job) @job = job check(queue_adapter.enqueued_jobs) end end + + # @private + class HavePerformedJob < Base + def initialize(job) + super("perform", "performed") + @job = job + end + + def matches?(proc) + raise ArgumentError, "have_performed_job only supports block expectations" unless Proc === proc + + original_performed_jobs_count = queue_adapter.performed_jobs.count + proc.call + in_block_jobs = queue_adapter.performed_jobs.drop(original_performed_jobs_count) + + check(in_block_jobs) + end + end + + # @private + class HaveBeenPerformed < Base + def initialize + super("perform", "performed") + end + + def matches?(job) + @job = job + check(queue_adapter.performed_jobs) + end + end end # @api public @@ -269,6 +305,75 @@ def have_been_enqueued ActiveJob::HaveBeenEnqueued.new end + # @api public + # Passes if a job has been performed inside block. May chain at_least, at_most or exactly to specify a number of times. + # + # @example + # expect { + # perform_jobs { HeavyLiftingJob.perform_later } + # }.to have_performed_job + # + # expect { + # perform_jobs { + # HelloJob.perform_later + # HeavyLiftingJob.perform_later + # } + # }.to have_performed_job(HelloJob).exactly(:once) + # + # expect { + # perform_jobs { 3.times { HelloJob.perform_later } } + # }.to have_performed_job(HelloJob).at_least(2).times + # + # expect { + # perform_jobs { HelloJob.perform_later } + # }.to have_performed_job(HelloJob).at_most(:twice) + # + # expect { + # perform_jobs { + # HelloJob.perform_later + # HeavyLiftingJob.perform_later + # } + # }.to have_performed_job(HelloJob).and have_performed_job(HeavyLiftingJob) + # + # expect { + # perform_jobs { + # HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42) + # } + # }.to have_performed_job.with(42).on_queue("low").at(Date.tomorrow.noon) + def have_performed_job(job = nil) + check_active_job_adapter + ActiveJob::HavePerformedJob.new(job) + end + + # @api public + # Passes if a job has been preformed. May chain at_least, at_most or exactly to specify a number of times. + # + # @example + # before { ActiveJob::Base.queue_adapter.performed_jobs.clear } + # before { ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true } + # + # HeavyLiftingJob.perform_later + # expect(HeavyLiftingJob).to have_been_performed + # + # HelloJob.perform_later + # HeavyLiftingJob.perform_later + # expect(HeavyLiftingJob).to have_been_performed.exactly(:once) + # + # 3.times { HelloJob.perform_later } + # expect(HelloJob).to have_been_performed.at_least(2).times + # + # HelloJob.perform_later + # HeavyLiftingJob.perform_later + # expect(HelloJob).to have_been_performed + # expect(HeavyLiftingJob).to have_been_performed + # + # HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42) + # expect(HelloJob).to have_been_performed.with(42).on_queue("low").at(Date.tomorrow.noon) + def have_been_performed + check_active_job_adapter + ActiveJob::HaveBeenPerformed.new + end + private # @private diff --git a/spec/rspec/rails/matchers/active_job_spec.rb b/spec/rspec/rails/matchers/active_job_spec.rb index f695b81a1a..b97b1aad6a 100644 --- a/spec/rspec/rails/matchers/active_job_spec.rb +++ b/spec/rspec/rails/matchers/active_job_spec.rb @@ -37,24 +37,21 @@ def to_global_id(options = {}) ActiveJob::Base.logger = original_logger end - let(:heavy_lifting_job) do - Class.new(ActiveJob::Base) do - def perform; end - end + class HeavyLiftingJob < ActiveJob::Base + def perform; end end + let(:heavy_lifting_job) { HeavyLiftingJob } - let(:hello_job) do - Class.new(ActiveJob::Base) do - def perform(*) - end + class HelloJob < ActiveJob::Base + def perform(*) end end + let(:hello_job) { HelloJob } - let(:logging_job) do - Class.new(ActiveJob::Base) do - def perform; end - end + class LoggingJob < ActiveJob::Base + def perform; end end + let(:logging_job) { LoggingJob } before do ActiveJob::Base.queue_adapter = :test @@ -308,4 +305,256 @@ def perform; end }.to raise_error(/expected to enqueue exactly 1 jobs, but enqueued 0/) end end + + describe "have_performed_job" do + before do + ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true + ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true + end + + it "raises ArgumentError when no Proc passed to expect" do + expect { + expect(heavy_lifting_job.perform_later).to have_performed_job + }.to raise_error(ArgumentError) + end + + it "passes with default jobs count (exactly one)" do + expect { + heavy_lifting_job.perform_later + }.to have_performed_job + end + + it "counts only jobs performed in block" do + heavy_lifting_job.perform_later + expect { + heavy_lifting_job.perform_later + }.to have_performed_job.exactly(1) + end + + it "passes when negated" do + expect { }.not_to have_performed_job + end + + it "fails when job is not performed" do + expect { + expect { }.to have_performed_job + }.to raise_error(/expected to perform exactly 1 jobs, but performed 0/) + end + + it "fails when too many jobs performed" do + expect { + expect { + heavy_lifting_job.perform_later + heavy_lifting_job.perform_later + }.to have_performed_job.exactly(1) + }.to raise_error(/expected to perform exactly 1 jobs, but performed 2/) + end + + it "reports correct number in fail error message" do + heavy_lifting_job.perform_later + expect { + expect { }.to have_performed_job.exactly(1) + }.to raise_error(/expected to perform exactly 1 jobs, but performed 0/) + end + + it "fails when negated and job is performed" do + expect { + expect { heavy_lifting_job.perform_later }.not_to have_performed_job + }.to raise_error(/expected not to perform exactly 1 jobs, but performed 1/) + end + + it "passes with job name" do + expect { + hello_job.perform_later + heavy_lifting_job.perform_later + }.to have_performed_job(hello_job).exactly(1).times + end + + it "passes with multiple jobs" do + expect { + hello_job.perform_later + logging_job.perform_later + heavy_lifting_job.perform_later + }.to have_performed_job(hello_job).and have_performed_job(logging_job) + end + + it "passes with :once count" do + expect { + hello_job.perform_later + }.to have_performed_job.exactly(:once) + end + + it "passes with :twice count" do + expect { + hello_job.perform_later + hello_job.perform_later + }.to have_performed_job.exactly(:twice) + end + + it "passes with :thrice count" do + expect { + hello_job.perform_later + hello_job.perform_later + hello_job.perform_later + }.to have_performed_job.exactly(:thrice) + end + + it "passes with at_least count when performed jobs are over limit" do + expect { + hello_job.perform_later + hello_job.perform_later + }.to have_performed_job.at_least(:once) + end + + it "passes with at_most count when performed jobs are under limit" do + expect { + hello_job.perform_later + }.to have_performed_job.at_most(:once) + end + + it "generates failure message with at least hint" do + expect { + expect { }.to have_performed_job.at_least(:once) + }.to raise_error(/expected to perform at least 1 jobs, but performed 0/) + end + + it "generates failure message with at most hint" do + expect { + expect { + hello_job.perform_later + hello_job.perform_later + }.to have_performed_job.at_most(:once) + }.to raise_error(/expected to perform at most 1 jobs, but performed 2/) + end + + it "passes with provided queue name" do + expect { + hello_job.set(:queue => "low").perform_later + }.to have_performed_job.on_queue("low") + end + + it "passes with provided at date" do + date = Date.tomorrow.noon + expect { + hello_job.set(:wait_until => date).perform_later + }.to have_performed_job.at(date) + end + + it "passes with provided arguments" do + expect { + hello_job.perform_later(42, "David") + }.to have_performed_job.with(42, "David") + end + + it "passes with provided arguments containing global id object" do + global_id_object = GlobalIdModel.new("42") + + expect { + hello_job.perform_later(global_id_object) + }.to have_performed_job.with(global_id_object) + end + + it "passes with provided argument matchers" do + expect { + hello_job.perform_later(42, "David") + }.to have_performed_job.with(42, "David") + end + + it "generates failure message with all provided options" do + date = Date.tomorrow.noon + message = "expected to perform exactly 2 jobs, with [42], on queue low, at #{date}, but performed 0" + \ + "\nQueued jobs:" + \ + "\n Class job with [1], on queue default" + + expect { + expect { + hello_job.perform_later(1) + }.to have_performed_job(hello_job).with(42).on_queue("low").at(date).exactly(2).times + }.to raise_error(message) + end + + it "throws descriptive error when no test adapter set" do + queue_adapter = ActiveJob::Base.queue_adapter + ActiveJob::Base.queue_adapter = :inline + + expect { + expect { heavy_lifting_job.perform_later }.to have_performed_job + }.to raise_error("To use ActiveJob matchers set `ActiveJob::Base.queue_adapter = :test`") + + ActiveJob::Base.queue_adapter = queue_adapter + end + + it "fails with with block with incorrect data" do + expect { + expect { + hello_job.perform_later("asdf") + }.to have_performed_job(hello_job).with { |arg| + expect(arg).to eq("zxcv") + } + }.to raise_error { |e| + expect(e.message).to match(/expected: "zxcv"/) + expect(e.message).to match(/got: "asdf"/) + } + end + + it "passes multiple arguments to with block" do + expect { + hello_job.perform_later("asdf", "zxcv") + }.to have_performed_job(hello_job).with { |first_arg, second_arg| + expect(first_arg).to eq("asdf") + expect(second_arg).to eq("zxcv") + } + end + + it "passess deserialized arguments to with block" do + global_id_object = GlobalIdModel.new("42") + + expect { + hello_job.perform_later(global_id_object, :symbolized_key => "asdf") + }.to have_performed_job(hello_job).with { |first_arg, second_arg| + expect(first_arg).to eq(global_id_object) + expect(second_arg).to eq({:symbolized_key => "asdf"}) + } + end + + it "only calls with block if other conditions are met" do + noon = Date.tomorrow.noon + midnight = Date.tomorrow.midnight + expect { + hello_job.set(:wait_until => noon).perform_later("asdf") + hello_job.set(:wait_until => midnight).perform_later("zxcv") + }.to have_performed_job(hello_job).at(noon).with { |arg| + expect(arg).to eq("asdf") + } + end + end + + describe "have_been_performed" do + before do + ActiveJob::Base.queue_adapter.performed_jobs.clear + ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true + ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true + end + + it "passes with default jobs count (exactly one)" do + heavy_lifting_job.perform_later + expect(heavy_lifting_job).to have_been_performed + end + + it "counts all performed jobs" do + heavy_lifting_job.perform_later + heavy_lifting_job.perform_later + expect(heavy_lifting_job).to have_been_performed.exactly(2) + end + + it "passes when negated" do + expect(heavy_lifting_job).not_to have_been_performed + end + + it "fails when job is not performed" do + expect { + expect(heavy_lifting_job).to have_been_performed + }.to raise_error(/expected to perform exactly 1 jobs, but performed 0/) + end + end end