Adarsh Pandit

Software Developer

Testing Active Job Error Handling

By Adarsh Pandit in programming

As I wrote yesterday, I’m a big fan of the new ActiveJob interface for background jobs in Rails.

As a dedicated Test-Driven Developer, I like to write tests first when developing features, including things happening in background jobs.

I have some errors appearing in the background jobs where I email lots of people for AsyncStandup (a side project using email to keep up to date with your team).

But exceptions were not being sent to Airbrake and debugging was difficult.

So I wrote a test using RSpec’s handy allow_any_instance_of stub:

describe DigestEmailJob, 'errors' do
  it 'reports errors' do
    error = StandardError.new('Hey! Something went wrong!')
    allow_any_instance_of(DigestEmailJob).to receive(:perform).
      and_raise(error)
    allow(Airbrake).to receive(:notify)

    DigestEmailJob.perform_now

    expect(Airbrake).to have_received(:notify).with(error)
  end
end

I stubbed the .perform method on any instance of the job class here, which is generally inadvisable. It is too broadly scoped to understand clearly what is stubbed and can add confusion to the tests. Also the RSpec maintainers advise caution:

This feature is sometimes useful when working with legacy code, though in general we discourage its use for a number of reasons:

  • The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example, in expect_any_instance_of(Widget).to receive(:name).twice it isn’t clear whether each specific instance is expected to receive name twice, or if two receives total are expected. (It’s the former.)
  • Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex.
  • It is the most complicated feature of rspec-mocks, and has historically received the most bug reports. (None of the core team actively use it, which doesn’t help.)

However, in this case, there isn’t a good alternative to simulating a raised exception, so we’ll proceed with caution.

To make the test pass, I added exception handling to ActiveJob using the rescue_from handler:

class DigestEmailJob < ActiveJob::Base
  def perform
    ...
  end

  rescue_from(StandardError) do |exception|
    Airbrake.notify(exception)
  end
end

Hat tip to Isaac Seymour for pointing out the any_instance_of solution. See the original discussion here. Thanks!

Also check out Isaac’s nice rspec-activejob matchers.

692dad2af6224bf856e94a96049292c4
Written by Adarsh Pandit

Read more posts by Adarsh, and follow Adarsh on Twitter.