A little gotcha in RSpec
If you've ever worked with RSpec to test ActiveJob jobs, you're probably familiar with testing a job performed with specific arguments.
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with enqueued job" do
ActiveJob::Base.queue_adapter = :test
expect {
UploadBackupsJob.perform_later("users-backup.txt", "products-backup.txt")
}.to have_enqueued_job.with("users-backup.txt", "products-backup.txt")
end
end
Now, if your arguments aren't available before running expect
, you'll need to use lazy evaluation with a block.
require "rails_helper"
RSpec.describe UploadBackupsJob do
it "matches with enqueued job" do
ActiveJob::Base.queue_adapter = :test
expect {
UploadBackupsJob.perform_later('backups.txt', rand(100), 'uninteresting third argument')
}.to have_enqueued_job.with { |file_name, seed|
expect(file_name).to eq 'backups.txt'
expect(seed).to be < 100
}
end
end
(Examples are from the official documentation)
Here, you might notice that .with
uses curly braces instead of a do ... end
block. The {}
with expect
is easily explained because a chaining method is called after that. But why doesn't .with
use do ... end
even when the block content is multiline? As a fan of Rubocop, I got used to the default block delimiter style, so I automatically replaced the braces with do ... end
without checking if it works.
expect {
UploadBackupsJob.perform_later('backups.txt', rand(100), 'uninteresting third argument')
}
.to have_enqueued_job(UploadBackupsJob)
.with do |file_name, seed|
expect(file_name).to eq 'backups.txt'
expect(seed).to be < 100
end
And... the test passed! Unfortunately, later, I discovered it was a false negative. The block wasn't executed at all!
It took me a while to understand the difference.
Firstly, {}
has higher precedence than do ... end
. You can check this in the Ruby documentation.
{ ... } blocks have priority below all listed operations, but do ... end blocks have lower priority.
Secondly, the .to
method accepts a block and passes it to the matcher's .matches?
method.
So it turns out the code was interpreted like this:
expect {
UploadBackupsJob.perform_later('backups.txt', rand(100), 'uninteresting third argument')
}
.to(have_enqueued_job(UploadBackupsJob).with) do |file_name, seed|
expect(file_name).to eq 'backups.txt'
expect(seed).to be < 100
end
And the matcher (in this case, RSpec::Rails::Matchers::ActiveJob::HaveEnqueuedJob
) simply ignored the block (no yield
)
That's it. I will use {}
in this case for now, but I might forget again someday. So, if I can muster the energy, I'll try to write a cop for it.
Discussion