rspec-sidekiq

repository·main·Indexed 20 days ago

https://github.com/wspurgin/rspec-sidekiq

A testing library for RSpec providing specialized matchers to verify Sidekiq job enqueuing, job options, and the contents of Sidekiq sets (Scheduled, Retry, and Dead sets). It includes tools for mocking Sidekiq Pro Batches, stubbing named queues without Redis, and testing sidekiq_retries_exhausted blocks. Key matchers include enqueue_sidekiq_job, have_enqueued_sidekiq_job, have_job, and various job option matchers like be_retryable and be_processed_in.

Tokens
7.7K
Snippets
22
Records
22
Agent score
70%

What's inside rspec-sidekiq

  1. Mock Sidekiq Pro Batches

    main

    For Sidekiq Pro users, you can opt-in to mocking the Batch implementation by setting stub_batches: true. This uses a NullObject pattern, allowing you to test code that uses Sidekiq::Batch without requiring a Redis instance.

    Caution: Because it uses a NullObject pattern, the mocked Sidekiq::Batch implementation responds to any method call. It does not provide the exact API of the real Sidekiq::Batch, so calling non-existent methods on the batch object will not raise a NoMethodError unless you explicitly expect it (as shown in the example).

    RSpec.describe "Using mocked batches", stub_batches: true do
      it "uses mocked batches" do
        batch = Sidekiq::Batch.new
        batch.jobs do
          SomeJob.perform_async 123
        end
    
        expect(SomeJob).to have_enqueued_sidekiq_job
    
        # Caution, the NullObject pattern means that the mocked Batch implementation
        # responds to anything... even if it's not on the true `Sidekiq::Batch` API
        expect { batch.foobar! }.to raise_error(NoMethodError)
      end
    end
  2. Install rspec-sidekiq

    main

    Add rspec-sidekiq to your :test group in your Gemfile.

    Note: This gem automatically requires sidekiq/testing. This changes Sidekiq's behavior so that enqueued jobs are pushed to a local job array instead of Redis. Because of this side effect, you should only include this gem in environments where this testing behavior is desired, such as your test group.

    # Gemfile
    group :test do
      gem 'rspec-sidekiq'
    end
  3. Configure rspec-sidekiq

    main

    You can customize the behavior of rspec-sidekiq by using the RSpec::Sidekiq.configure block in your spec_helper.rb.

    Available configuration options:

    • clear_all_enqueued_jobs: If true, clears all job queues before each example (default: true).
    • enable_terminal_colours: If true, uses terminal colors when outputting messages (default: true).
    • warn_when_jobs_not_processed_by_sidekiq: If true, issues a warning when jobs are enqueued to a local job array instead of Redis (default: true).
    RSpec::Sidekiq.configure do |config|
      # Clears all job queues before each example
      config.clear_all_enqueued_jobs = true # default => true
    
      # Whether to use terminal colours when outputting messages
      config.enable_terminal_colours = true # default => true
    
      # Warn when jobs are not enqueued to Redis but to a job array
      config.warn_when_jobs_not_processed_by_sidekiq = true # default => true
    end
  4. Stub Sidekiq named queues without Redis

    main

    If you need to test jobs within Sidekiq's named sets (ScheduledSet, RetrySet, or DeadSet) without a running Redis instance, you can opt-in by setting stub_named_queues: true in your RSpec configuration. This replaces the actual Redis-backed sets with in-memory implementations managed by a JobStore.

    To interact with the underlying data, you can access the store via RSpec::Sidekiq::NamedQueues.job_store to manually add retry or dead jobs for testing purposes.

    RSpec.describe "Scheduled jobs", stub_named_queues: true do
      it "schedules a job" do
        AwesomeJob.perform_at(1.hour.from_now, 'arg')
    
        expect(Sidekiq::ScheduledSet.new).to have_job(AwesomeJob).with('arg')
      end
    
      it "tracks retry jobs via the job store" do
        store = RSpec::Sidekiq::NamedQueues.job_store
        store.add_retry(
          "class" => "AwesomeJob",
          "args" => ["arg"],
          "error_message" => "boom",
          "error_class" => "RuntimeError",
          "retry_count" => 1
        )
    
        expect(Sidekiq::RetrySet.new)
          .to have_job(AwesomeJob)
          .with_error('boom')
          .with_retry_count(1)
      end
    
      it "tracks dead jobs via the job store" do
        store = RSpec::Sidekiq::NamedQueues.job_store
        store.add_dead(
          "class" => "AwesomeJob",
          "args" => ["arg"],
          "failed_at" => Time.now.to_f
        )
    
        expect(Sidekiq::DeadSet.new)
          .to have_job(AwesomeJob)
          .died_within(1.minute)
      end
    end
  5. Mock Sidekiq::Batch to test without Redis

    main

    Since Sidekiq::Batch is a Sidekiq Pro feature that requires Redis, rspec-sidekiq provides an opt-in mocking mechanism using a "null object" pattern. This allows you to test code that uses batches without a running Redis instance.

    To enable this, add stub_batches: true to your RSpec example group metadata. When enabled, Sidekiq::Batch.new will return a NullBatch object that simulates batch behavior (including jobs blocks and on callbacks) without interacting with Redis.

    RSpec.describe "Using mocked batches", stub_batches: true do
      it "uses mocked batches" do
        batch = Sidekiq::Batch.new
        batch.jobs do
          SomeJob.perform_async 123
        end
    
        expect(SomeJob).to have_enqueued_sidekiq_job
      end
    end
  6. Use `have_job` to inspect Sidekiq Sets (Scheduled, Retry, Dead)

    main

    The have_job matcher is used to verify that a specific job exists within a Sidekiq Set (such as ScheduledSet, RetrySet, or DeadSet). This is useful for testing the state of Sidekiq's internal management sets.

    Capabilities

    • Filtering: Match by job class, specific arguments, or a glob-style pattern using .scanning(pattern) against the job's JSON representation.
    • RetrySet Specifics: Verify error details using .with_error(message), .with_error_class(exception_class), and .with_retry_count(n).
    • DeadSet Specifics: Verify when a job died using .died_within(interval).
    • Frequency: Supports .once, .twice, .exactly(n), .at_least(n), and .at_most(n).
    # Match any job in the set
    expect(Sidekiq::ScheduledSet.new).to have_job
    
    # Match a specific job class and arguments
    expect(Sidekiq::ScheduledSet.new).to have_job(AwesomeJob).with('arg')
    
    # Testing retry jobs
    expect(Sidekiq::RetrySet.new)
      .to have_job(AwesomeJob)
      .with('arg')
      .with_error('something went wrong')
      .with_error_class(RuntimeError)
      .with_retry_count(2)
    
    # Testing dead jobs
    expect(Sidekiq::DeadSet.new)
      .to have_job(AwesomeJob)
      .with('arg')
      .died_within(1.hour)
    
    # Scanning with a pattern (glob-style)
    expect(Sidekiq::ScheduledSet.new).to have_job(AwesomeJob).scanning("*some_trace_id*")
  7. Use `enqueue_sidekiq_job` to test job enqueuing in blocks

    main

    The enqueue_sidekiq_job matcher describes that a block of code should enqueue a Sidekiq job. It is highly composable and allows you to specify the job class, arguments, queue, timing, and frequency.

    Common Usage Patterns

    • Basic enqueuing: Check if any job is enqueued.
    • Specific class: Specify which job class is expected.
    • Arguments: Use .with(...) to match specific arguments.
    • Queue: Use .on(queue_name) to verify the target queue.
    • Timing: Use .at(time) for specific datetimes or .in(interval) for relative intervals.
    • Frequency: Use .once, .never, .exactly(n), .at_least(n), or .at_most(n) to verify how many times a job is enqueued.
    • Context: Use .with_context(hash) to verify job options set via .set(...) (e.g., retry, trace_id).
    # Basic
    expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job
    
    # A specific job class
    expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job(AwesomeJob)
    
    # with specific arguments
    expect { AwesomeJob.perform_async "Awesome!" }.to enqueue_sidekiq_job.with("Awesome!")
    
    # On a specific queue
    expect { AwesomeJob.set(queue: "high").perform_async }.to enqueue_sidekiq_job.on("high")
    
    # At a specific datetime
    specific_time = 1.hour.from_now
    expect { AwesomeJob.perform_at(specific_time) }.to enqueue_sidekiq_job.at(specific_time)
    
    # In a specific interval
    freeze_time do
      expect { AwesomeJob.perform_in(1.hour) }.to enqueue_sidekiq_job.in(1.hour)
    end
    
    # Frequency examples
    expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.once
    expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.at_least(1).time
    expect { AwesomeJob.perform_async }.to enqueue_sidekiq_job.at_most(2).times
    
    # With specific context (e.g. overrides via .set)
    expect {
      AwesomeJob.set(retry: 5).perform_async
    }.to enqueue_sidekiq_job.with_context(retry: 5)
    
    # Combining matchers
    expect { AwesomeJob.perform_at(specific_time, "Awesome!") }.to(
      enqueue_sidekiq_job(AwesomeJob)
      .with("Awesome!")
      .on("default")
      .at(specific_time)
    )
  8. Test code inside sidekiq_retries_exhausted blocks

    main

    To test logic contained within a sidekiq_retries_exhausted block, use the within_sidekiq_retries_exhausted_block helper. This allows you to wrap your expectations around the execution of the block to ensure the correct side effects occur when retries are exhausted.

    # Implementation code:
    sidekiq_retries_exhausted do |msg|
      bar('hello')
    end
    
    # Test code:
    FooClass.within_sidekiq_retries_exhausted_block {
      expect(FooClass).to receive(:bar).with('hello')
    }
  9. Verify Sidekiq job class options

    main

    These matchers allow you to verify the sidekiq_options defined on a job class.

    • have_job_option(option, value): Verifies a single option (e.g., :retry, :queue, :dead).
    • have_job_options(hash): Verifies multiple options at once.
    • be_processed_in(queue): Verifies the queue the job is configured to use.
    • be_retryable(boolean_or_count): Verifies the retry option. Note: to test overrides applied via .set(...) at runtime, use with_context instead.
    • save_backtrace(boolean_or_count): Verifies the backtrace option.
    • be_unique: Verifies Sidekiq Enterprise unique job configuration. Supports .for(interval) and .until(condition) (for Enterprise).
    • be_expired_in(interval): Verifies the expires_in option.
    class AwesomeJob
      include Sidekiq::Job
      sidekiq_options retry: 5, queue: 'critical', backtrace: true
    end
    
    expect(AwesomeJob).to have_job_option(:retry, 5)
    expect(AwesomeJob).to have_job_option(:queue, 'critical')
    expect(AwesomeJob).to have_job_options(retry: 5, queue: 'critical', backtrace: true)
    
    expect(AwesomeJob).to be_processed_in :critical
    expect(AwesomeJob).to be_retryable 5
    expect(AwesomeJob).to save_backtrace 5
    expect(AwesomeJob).to be_unique
    expect(AwesomeJob).to be_unique.for(1.hour)
    expect(AwesomeJob).to be_expired_in 1.hour
  10. Use `have_enqueued_sidekiq_job` to verify enqueued jobs

    main

    The have_enqueued_sidekiq_job matcher describes that a specific job should already be in the enqueued state. Unlike enqueue_sidekiq_job, this is typically called on the Job class itself rather than a block.

    Key Features

    • Argument Matching: Supports standard RSpec argument matchers like hash_including, any_args, or hash_excluding.
    • Frequency: Supports .once, .exactly(n), .at_least(n), and .at_most(n).
    • Context: Use .with_context(hash) to check job options.
    • Scheduling: Use .at(time), .in(interval), or .immediately (for jobs scheduled in the past).
    • Queues: Use .on(queue_name) to verify the queue.
    • ActiveMailer: Can be used to test deliver_later calls by matching the underlying Sidekiq job structure (e.g., Sidekiq::Worker, class name, method name, and arguments).
    # Basic argument matching
    expect(AwesomeJob).to have_enqueued_sidekiq_job('Awesome', true)
    
    # Using RSpec mocks matchers
    expect(AwesomeJob).to have_enqueued_sidekiq_job(hash_including("something" => "Awesome"))
    
    # Frequency
    expect(AwesomeJob).to have_enqueued_sidekiq_job.once
    
    # Context
    expect(AwesomeJob).to have_enqueued_sidekiq_job.with_context(trace_id: anything)
    
    # Scheduling
    time = 5.minutes.from_now
    expect(AwesomeJob).to have_enqueued_sidekiq_job('Awesome', true).at(time)
    
    # Queue
    expect(AwesomeJob).to have_enqueued_sidekiq_job("Very Awesome!").on("high")
    
    # ActiveMailer
    expect(Sidekiq::Worker).to have_enqueued_sidekiq_job(
      "AwesomeActionMailer",
      "invite",
      "deliver_now",
      user,
      true
    )
  11. Configure RSpec::Sidekiq settings

    main

    The RSpec::Sidekiq::Configuration object allows you to control how Sidekiq jobs are handled during your RSpec tests. You can configure whether job queues are cleared between examples, whether terminal colors are enabled for output, and whether warnings are issued when jobs are enqueued to a local array instead of Redis.

    # Example of accessing configuration attributes
    config = RSpec::Sidekiq::Configuration.new
    
    config.clear_all_enqueued_jobs = false
    config.enable_terminal_colours = false
    config.warn_when_jobs_not_processed_by_sidekiq = false
  12. Compose multiple enqueue_sidekiq_job assertions

    main

    Because enqueue_sidekiq_job is a standard RSpec matcher, you can compose multiple assertions within a single expect block using .and to verify that multiple different jobs are enqueued during the execution of the block.

    expect do
      AwesomeJob.perform_async
      OtherJob.perform_async
    end.to enqueue_sidekiq_job(AwesomeJob).and enqueue_sidekiq_job(OtherJob)