Posts Tagged ‘delayed-job’

Custom rspec matcher for Delayed::Job performable methods

Tuesday, May 22nd, 2012

I’ve been working on code that delays a bunch of work using Delayed Job’s :delay method. Stuff like this:

class Album
  after_create :send_emails
 
  def send_emails
    recipients.each do |recipient|
      recipient.delay.send_email
    end
  end
end

I tried a number of testing strategies:

  • checking that the Delayed::Job.count changed
  • testing for properties on Delayed::Job.last
  • working off the jobs and testing that the work got done.

But they all seemed too indirect. In the end I decided to just write a custom matcher that lets me do this:

expect { 
  Album.create :recipients => [janelle_monáe]
}.to delay_method(janelle_monáe, :send_email)

Another reasonable route would be to mock :delay on the appropriate object, and then do a :should_receive on the mock. That only works if the object that’s getting the method delayed exists in your test scope. It won’t work if the object is created or looked up inside the method you’re testing…. unless you stub out the relevant :create and :find methods.

I don’t know. I’m still (3+ years in) learning how to decide how intensively to mock my unit tests. Some part of me thinks I’m just not using mocks enough. But the part that likes unit tests to be a little more flexible in the face of refactoring resists. We’ll see how it goes.

Use it like this:

# On a specific object:
apple = Apple.new
expect {...}.to delay_method(apple, :core)
 
# On any instance of a class:
expect {...}.to delay_method(Apple.any_instance, :core)
 
# On whatever object the code returns:
expect { Apple.create }.to delay_method(returned_object, :core)

And here’s the code:

# spec/support/delayed_job.rb
module RspecDelayedJobMatcher
  class ReturnedObject
  end
end
 
def returned_object
  RspecDelayedJobMatcher::ReturnedObject
end
 
RSpec::Matchers.define :delay_method do |object, method_name|
 
  def jobs
    Delayed::Job.all.select do |job|
      (payload = job.payload_object) && equivalent_object?(payload.object) && payload.method_name == @method_name
    end.count
  end
 
  def equivalent_object?(payload)
    if @object == returned_object
      payload == @response
    elsif @object.is_a? RSpec::Mocks::AnyInstance::Recorder
      payload.is_a? @object.instance_variable_get("@klass")
    else
      payload == @object
    end
  end
 
  def description
    if @object == returned_object
      "the returned object (#{@response.inspect})"
    elsif @object.is_a? RSpec::Mocks::AnyInstance::Recorder
      "some instance of #{@object.instance_variable_get("@klass").to_s}"
    else
      @object.inspect
    end
  end
 
  match do |proc|
    @method_name = method_name
    @object = object
    count_before = jobs
    @response = proc.call
    (@count = jobs - count_before) == 1
  end
 
  failure_message_for_should do |actual|
    "Expected block to create a job to invoke #{@method_name} on #{description}, but it created #{@count}"
  end
 
  failure_message_for_should_not do |actual|
    "Block created #{@count} job(s) to invoke #{@method_name} on #{description}, but expected none"
  end
 
  description do
    "expect block to delay #{@method_name} on a #{@object.class.to_s}"
  end
end

Bizarro Delayed::Job/ActiveRecord/Callback/Validation bug

Friday, June 3rd, 2011

Been dealing with at truly weird bug. I just upgraded to Rails 3 and my rake tasks are failing with:

wrong number of arguments (0 for 1)

With no line numbers, no help, no nothing even if I run “rake jobs:work –trace”.

After much agony, I narrowed the issue down to the :valid? method on my ActiveRecord objects. The actual Rails code, not my code. The interwebs seemed to suggest that there could be some issue with using deprecated validation apis (overwriting validate, for example) but I wasn’t doing that.

I eventually debugged into ActiveRecord, and discovered the problem. Check this out:

# activerecord-3.0.7/lib/active_record/validations.rb:53
def valid?(context = nil)
  context ||= (new_record? ? :create : :update)
  output = super(context)
 
  deprecated_callback_method(:validate)
  deprecated_callback_method(:"validate_on_#{context}")
 
  errors.empty? && output
end

In there we call deprecated_callback_method(:validate), but deprecated_callback_method will just execute whatever symbol you give it. So it sends :validate with no parameters. But new versions of ActiveRecord::Base expect validate to come with some sort of parameters.

On what planet is that supposed to work?

Well, it turns out if I run the exact code in my rails console, it works just fine. In a Delayed::Job task, User.first.valid? raises an ArgumentError, but in the rails console it just returns true.

Seriously. What the fuck.

My hackety-hack monkeypatch is just to throw this in my environment:

module ActiveRecord
  module Callbacks
    def deprecated_callback_method(symbol)
    end
  end
end

Which seems like it could be potentially bad, but if the deprecated callback method is deprecated, why would I want to use it anyway? Like I said, I don’t think I’m actually using any deprecated callbacks, but who really knows.

My job queue works again. I’m moving on.

Incorporating Delayed Job in rspec specs

Friday, March 5th, 2010

. . .

Update: The preferred way to do this these days is simply: Delayed::Worker.new.work_off

. . .

I wanted to test the results of some Delayed Job enabled methods in my specs. DJ has a nice method Delayed::Job.work_off that does all of the jobs on the queue. Unfortunately, it does them in a separate thread, so if you run it from rspec and there are errors in your jobs, rspec never hears about them. My solution was to write an even simpler, in-thread job runner: (edit: updated to delete the job too, at Damien’s suggestion)

# lib/delayed_job_spec_helper.rb
module DelayedJobSpecHelper
  def work_off
    Delayed::Job.all.each do |job|
      job.payload_object.perform
      job.destroy
    end
  end
end

My spec might look like this:

describe Thingy do
  include DelayedJobSpecHelper
 
  it "should have been worked on if I do something that queues jobs" do
    thing = Thingy.new
    thing.method_that_queues_jobs
    work_off
    thing.should be_worked_on
  end
end

Voila.