Sidekiq Testing

tl;dr Make your tests better with Sidekiq’s built-in testing support.

Say I’ve got a Sidekiq worker that takes a list of numbers, and schedules another worker to handle the processing of each number.

class DelegatorWorker
  include Sidekiq::Worker

  def perform(args)
    Array(args["numbers"]).each do |number|
      DelegateWorker.perform_async("number" => number)
    end
  end
end

How do I test this? With RSpec, I’d be tempted to use its amazing test double facilities.

RSpec.describe DelegatorWorker do
  describe "#perform" do
    it "delegates to DelegateWorker" do
      allow(DelegateWorker).to receive(:perform_async)
      described_class.new.perform("numbers" => [1, 2])
      expect(DelegateWorker).to have_received(:perform_async).with(1)
      expect(DelegateWorker).to have_received(:perform_async).with(2)
    end
  end
end

And that’s fine as far as it goes. I’m asserting that DelegatorWorker conforms to Sidekiq’s calling interface (the perform_async method). I’ve mocked something I don’t own, but it’s not an unreasonable tradeoff for such a simple situation. But what happens if I want to add some job-related metadata to each delegated job? Maybe I’ve got some Sidekiq middleware that handles odd numbers differently.

class DelegatorWorker
  include Sidekiq::Worker

  def perform(args)
    Array(args["numbers"]).each do |number|
      DelegateWorker.set("odd" => number.odd?)
                    .perform_async("number" => number)
    end
  end
end

The tests won’t break, because DelegateWorker.set returns the worker class. But integrating tests for that set call will get gnarly. I could get fancier with RSpec, maybe return some spies when set is called with specific arguments, but that kind of test cruft piles up quickly, and impairs the readability of the test.

Fortunately, Sidekiq itself provides a good answer with its built-in testing support. Let’s rewrite the original test using Sidekiq stuff, without regard to the new odd-number requirement:

RSpec.describe DelegatorWorker do
  describe "#perform" do
    it "delegates to DelegateWorker" do
      described_class.new.perform("numbers" => [1, 2])
      expect(DelegateWorker.jobs).to include(
        hash_including("args" => [{ "number" => 1 }]),
        hash_including("args" => [{ "number" => 2 }])
      )
    end
  end
end

Note the secret sauce, which is the jobs method that Sidekiq’s testing module adds to Sidekiq workers. I still get to flex some RSpec-fu too, using hash_including to focus on only the pertinent parts of the job. Far more importantly, the test is quite readable. There’s no test-double cruft, just clean execution and assertion.

Asserting the new odd setting is trivial:

RSpec.describe DelegatorWorker do
  describe "#perform" do
    it "delegates to DelegateWorker" do
      described_class.new.perform("numbers" => [1, 2])
      expect(DelegateWorker.jobs).to include(
        hash_including("odd" => true, "args" => [{ "number" => 1 }]),
        hash_including("odd" => false, "args" => [{ "number" => 2 }])
      )
    end
  end
end

There’s plenty more good stuff in Sidekiq where that came from. Do yourself a favor and leverage it in your tests.

Questions? Comments? Contact me!

Tools Used

RSpec
3.9.0
Ruby
2.6.5p114
Sidekiq
6.0.3