Home > database >  RSpec: how to chain receive().with()?
RSpec: how to chain receive().with()?

Time:11-26

I've been writing tests with instance_doubles to stand in for message chains when I need more granularity in the midst of the chain. But, I'm wondering if I'm doing things the hard way.

Here's the method I want to test:

def run_produceable_job
  # Delete any jobs that exist, but haven't started, in favor of this new job
  Delayed::Job.where(queue: 'produceable', locked_at: nil).delete_all

  ProduceableJob.perform_later
end

For the Delayed::Job call, it's important I check that the queue name is as expected. I also want to make sure that Delayed::Job is receiving .delete_all at the end

I would like to do something like this:

expect(Delayed::Job).to receive(:where).with(queue: 'produceable', locked_at: nil).and_then_receive(:delete_all)
                                                                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Does RSpec offer some sort of chaining for receive? I've skimmed the docs, but can't find anything that specifically talks about adding multiple receives.

Or am I going to have to do it the long way?

ar_relation = instance_double ActiveRecord::Relation
allow(Delayed::Job).to receive(:where).with(queue: 'produceable', locked_at: nil).and_return(ar_relation)
allow(ar_relation).to receive(:delete_all)

expect(Delayed::Job).to receive(:where).with(queue: 'produceable', locked_at: nil)
expect(ar_relation).to receive(:delete_all)

CodePudding user response:

IMHO you have to go the long way. There is no shorter way to describe it.

Regardless of that, I would recommend you overthink your testing strategy. At the moment you test that a very specific combination of methods is called but not if these method calls are actually doing what you want them to do.

Instead, I would create an example record that should be deleted (and perhaps a couple that should not be deleted), then run the job and afterward test that only the expected record was deleted.

For example like this:

let!(:record_to_be_deleted) { Delayed::Job.create!(queue: 'produceable', locked_at: nil) }
let!(:records_to_stay) do
  [ 
    Delayed::Job.create!(queue: 'produceable', locked_at: Time.current),
    Delayed::Job.create!(queue: 'default', locked_at: nil)
  ]
end

it "should remove only expected records" do
  expect {
    instance.run_produceable_job
  }.to chance { DelayedD::Job.count }.from(3).to(2)

  expect { 
    record_to_be_deleted.reload
  }.to raise_error(ActiveRecord::RecordNotFound)
end

The rule of thumb is to test the expected outcome, not the specific implementation. Because the implementation might change, will be refactored or might break in future versions.

  • Related