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)
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 { Delayed::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.