29

I have some complex, long-running delayed_job processes in my application. I am using Rspec to test individual methods and classes used in the processes, but I would also like to perform many of end-to-end background jobs, with different test data.

I couldn't find anything on the delayed_job wiki on this, and this SO question looks interesting but I didn't really understand what is happening here. What's the best way to test delayed_job chains with rSpec?

I can easily set up the test data with a factory, and then call the class that starts the background processing. I expect the tests to take a long time to complete.

edited background code

class Singleplex
    def perform(batch_id,user)   
      batch = start_batch(batch_id,user)
        ... do lots of stuff ...
    end
    handle_asynchronously :perform, queue: :singleplex, :run_at => Proc.new { 1.second.from_now }

spec/factories/batches.rb

FactoryGirl.define do
  factory :batch do
    batch_type 'singleplex'
    name 'valid panel'
    status 'ready'
  end

  factory :batch_detail do
    chrom 7
    chrom_start 140435012
    chrom_end 140435012
    target_offset 150
    padding 4
    primer3_parameter_id 1
    snp_mask 't'
    status 'ready'
    batch
  end
end

Then run the test like this

describe Batch do  
  it 'runs Singleplex for a valid panel' do
    batch = FactoryGirl.create(:batch)
    user = User.find(1)
    status =  Singleplex.new.perform(batch.id,user)
    expect(status.should == true)
  end
end

I have two problems to solve:

1) How to tell the test to wait until the delayed_job call is completed before validating the results ?

2) To validate the results I will need to check values in multiple tables. What is the best way to do this in Rspec?

EDIT

I should add I get a delayed_job object, so of course the status check fails. The jobs take at least 10 minutes usually.

1) Batch runs Singleplex for a valid panel
     Failure/Error: expect(status.should == true)
       expected: true
            got: #<Delayed::Backend::ActiveRecord::Job id: nil, priority: 0, attempts: 0, handler: "--- !ruby/object:Delayed::PerformableMethod\nobject:...", last_error: nil, run_at: nil, locked_at: nil, failed_at: nil, locked_by: nil, queue: nil, created_at: nil, updated_at: nil> (using ==)
Community
  • 1
  • 1
port5432
  • 5,889
  • 10
  • 60
  • 97
  • 2
    By the way, it looks like you're mixing RSpec's `should` and `expect` syntax. `expect(status.should == true)` should be either `status.should == true` or `expect(status).to == true` – fny Nov 03 '14 at 19:55

3 Answers3

42

There are a few ways to go about doing this. All of them require that you execute the job in your code.

Method 1: A test which queues the job and then tells the DelayedJob::Worker to complete it.

describe Batch do  
  it 'runs Singleplex for a valid panel' do
    batch = FactoryGirl.create(:batch)
    user = User.find(1)
    Singleplex.new.perform(batch.id,user)
    expect(Delayed::Worker.new.work_off).to eq [1, 0] # Returns [successes, failures]
    # Add expectations which check multiple tables to make sure the work is done
  end
end

Method 2: A test which runs the job in question with queueing disabled, and checks for the desired results. You can delay queuing by calling Delayed::Worker.delay_jobs = false somewhere in your testing configuration or in a before block.

before(:each) do
  Delayed::Worker.delay_jobs = false
end
describe Batch do  
  it 'runs Singleplex for a valid panel' do
    batch = FactoryGirl.create(:batch)
    user = User.find(1)
    Singleplex.new.perform(batch.id,user)
    # expectations which check that the work is done
  end
end

This method has, however, been known to cause issues with callbacks.

Method 3: Write an observer that watches for any new jobs that are created and runs them. This way you won't have to manually declare "work_off" in your tests. Artsy has a gist for this.

It's also a good idea to have tests elsewhere that make sure jobs get queued as expected

it "queues welcome when a user is created" do
  expect(Delayed::Job.count).to eq 0
  # Create user step
  expect(Delayed::Job.count).to eq 1 # You should really be looking for the count of a specific job.
end
hansottowirtz
  • 664
  • 1
  • 6
  • 16
fny
  • 31,255
  • 16
  • 96
  • 127
  • Thanks @faraz, for method 3, where did you add the references to `delayed_job_observer.rb`, is it in `spec_helper.rb`? – netwire Mar 06 '15 at 08:34
  • Use `rails_helper.rb` if you're using the newer organization format, otherwise `spec_helper.rb` works. – fny Mar 06 '15 at 14:20
  • in your first method, shouldn't the job be performed right away, it won't queue it right? – jshah Dec 06 '17 at 22:26
  • @jshah The job will not be performed right away since there's no background worker running to consume the job during tests unless, for whatever crazy reason, you decided that running a background worker is a good idea. – fny Dec 07 '17 at 16:45
16

If you want to run delayed job around single test or a set of tests, you can add this to your spec_helper.rb

config.around(:each, :run_delayed_jobs) do |example|
  Delayed::Worker.delay_jobs = false

  example.run

  Delayed::Worker.delay_jobs = true
end

And call it with:

it 'runs the job', :run_delayed_jobs do
  # delayed job magic
end
stevenspiel
  • 5,775
  • 13
  • 60
  • 89
0

I think anyone who use delayed job will face with this issue. In my case, I send email notifications in an asynchronous mode by using delayed job. When doing unit test, I usually need to verify if the mailer enqueue work has been done or not. In order to handle this, I write a custom matcher for Rspec like below:

  RSpec::Matchers.define :have_created_delayed_job do |method_name|
    match do |_|
      Delayed::Job.all.find do |job|
        # rubocop:disable Security/YAMLLoad
        YAML.load(job.handler).method_name == method_name
        # rubocop:enable Security/YAMLLoad
      end
    end
    failure_message do |subject|
      "Expected that #{subject} would have created a delayed job with method name #{method_name}.\n
      But we didn't find the matching job."
    end
  end

Then in your testing code, you can do it like this:

expect(subject).to have_created_delayed_job(:your_method_name)
hiveer
  • 660
  • 7
  • 17