I am trying to write a rspec for delayed job.
Currently I use delayed job as follows:
class IncomingMailsController < ApplicationController
...
MailingJob.new(#incoming_mail.id).perform
...
end
Then in /lib/mailing_job.rb:
class MailingJob < Struct.new(:mailing_id)
def perform
.......
How can I test that with rspec? Right now I have:
/spec/lib/mailing_job/mailingjob_spec.rb
require 'spec_helper'
describe MailingJob do
include DelayedJobSpecHelper
it "should have been worked on if I do something that queues jobs" do
#incoming_mail = IncomingMail.create(.........)
MailingJob.new(#incoming_mail.id).perform
#IncomingMail.method_that_queues_jobs
work_off
MailingJob.should be_worked_on
end
end
/spec/lib/delayed_job_spec_helper.rb
module DelayedJobSpecHelper
# http://erikonrails.snowedin.net/?p=230
def work_off
Delayed::Job.all.each do |job|
job.payload_object.perform
job.destroy
end
end
end
But this errors with:
1) MailingJob should have been worked on if I do something that queues jobs
Failure/Error: MailingJob.should be_worked_on
NoMethodError:
undefined method `worked_on?' for MailingJob:Class
# ./spec/lib/mailing_job/mailingjob_spec.rb:19
Ideas? Thanks
You're queueing your job incorrectly in the controller. perform is called by the worker, not by you.
Delayed::Job.enqueue MailingJob.new(#incoming_mail.id)
Similarly, don't call perform in your spec, that's what work_off does.
The structure of the spec should be this:
Access controller method that queues the job
Tell DelayedJob to work off the queue
Check to see that it did what it was supposed to do
I'm not sure where be_worked_on comes in, as worked_on? is not defined anywhere in DelayedJob (and indeed, is the cause of your error). I would instead check something was done, like a mail was sent, or whatever your job is supposed to do.
Related
I am using the most basic version of delayed_job in a Rails app. I have the max time allowed for a delayed_job set at 10 minutes. I would like to get the hooks/callbacks working so I can do something after a job stop executing at the 10 minute mark.
I have this set in my rails app:
config.active_job.queue_adapter = :delayed_job
This is how I normally queue a job:
object.delay.object_action
The hook/callback example is for a named job but the basic, getting started steps are not for a named job. So I don't think I have a named job. Here is the example given to get the callbacks working:
class ParanoidNewsletterJob < NewsletterJob
def enqueue(job)
record_stat 'newsletter_job/enqueue'
end
def perform
emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
end
def before(job)
record_stat 'newsletter_job/start'
end
def after(job)
record_stat 'newsletter_job/after'
end
def success(job)
record_stat 'newsletter_job/success'
end
def error(job, exception)
Airbrake.notify(exception)
end
def failure(job)
page_sysadmin_in_the_middle_of_the_night
end
end
I would love to get the after or error hooks/callbacks to fire.
Where do I put these callbacks in my Rails app to have them fire for the basic delayed_job setup? If I should be using ActiveJob callbacks where do you put those callbacks given delayed_job is being used?
You cannot use object.delay.object_action convenience syntax if you want more advanced features like callbacks. The #delay convenience method will generate a job object that works similar to this:
# something like this is already defined in delayed_job
class MethodCallerJob
def initialize(object, method, *args)
#object = object
#method = method
#args = args
end
def perform
#object.send(#method, *#args)
end
end
# `object.delay.object_action` does the below automatically for you
# instantiates a job with your object and method call
job = MethodCallerJob.new(object, :object_action, [])
Delayed::Job.enqueue(job) # enqueues it for running later
then later, in the job worker, something like the below happens:
job = Delayed::Job.find(job_id) # whatever the id turned out to be
job.invoke_job # does all the things, including calling #perform and run any hooks
job.delete # if it was successful
You have to create what the delayed_job README calls "Custom Jobs", which are just plain POROs that have #perform defined at a minimum. Then you can customize it and add all the extra methods that delayed_job uses for extra features like max_run_time, queue_name, and the ones you want to use: callbacks & hooks.
Sidenote: The above info is for using delayed_job directly. All of the above is possible using ActiveJob as well. You just have to do it the ActiveJob way by reading the documentation & guides on how, just as I've linked you to the delayed_job README, above.
You can create delayed_job hooks/callback by something like this
module Delayed
module Plugins
class TestHooks < Delayed::Plugin
callbacks do |lifecycle|
lifecycle.before(:perform) do |_worker, job|
.....
end
end
end
end
end
And need this plugin to initializer
config/initializers/delayed_job.rb
require_relative 'path_to_test_plugin'
Delayed::Worker.plugins << Delayed::Plugins::TestHooks
Similar to perform there are also hooks for success failure and error.
And similar to 'before' you can also capture the 'after' hooks.
I have installed the Ruby gem Delayed_Job to run tasks in a queue, but it shows some behavior that I don't understand. Delayed_Job is using my local active_record so a very standard installation.
I have the code for a job in a file called test_job.rb in my /lib folder
class TestJob
# Create a entry in the database to track the execution of jobs
DatabaseJob = Struct.new(:text, :emails) do
def perform
# Perform Test Code
end
end
def enqueue
#enqueue the job
Delayed::Job.enqueue DatabaseJob.new('lorem ipsum...', 'test email')
end
end
When I try to call the code from a controller like this, the first time the job seems to get submitted (is listed in rake jobs:work) but it does not run:
require 'test_job'
class ExampleController < ApplicationController
def index
end
def job
# Create a new job instance
job = TestJob.new
# Enqueue the job into Delay_Job
job.enqueue
end
end
Then when I change the controller code to do what my lib class does, it works perfectly. The job does not only get submitted to the queue, but also runs and completes without failures.
require 'test_job'
class ExampleController < ApplicationController
# Create a entry in the database to track the execution of jobs
DatabaseJob = Struct.new(:text, :emails) do
def perform
# Perform Test Code
end
end
def index
end
def job
#enqueue the job
Delayed::Job.enqueue DatabaseJob.new('lorem ipsum...', 'test email')
end
end
The strange thing is that when I switch back to calling the lib job class it works without a problem. Then it does not matter whether the struct is directly defined in the controller or in the class in the lib folder.
Defining the struct inside the controller and submitting the job to the queue this way always seems to work, but afterwards also the lib class starts working and sometimes the lib class works even after a restart of the rails server.
Any ideas? Thank you very much for the help.
Best,
Bastian
Below is passing!
Controller code:
class OrdersController
def create
...
#order.save
end
end
Spec code:
describe OrdersController do
it "should call save method" do
Order.any_instance.should_receive(:save)
post :create
end
end
But if only it were that easy... I have some custom job objects that are executed after the save, so the code actually looks like this:
Controller code:
class OrdersController
def create
...
#order.save
RoadrunnerEmailAlert.new.async.perform(#order.id, true)
CalendarInvite.new.async.perform(#order.id)
RoadrunnerTwilioAlert.new.async.perform(#order.id)
end
end
I would love to test that the custom objects are receiving the chain of methods with the right parameters, but not sure how, short of creating something in the spec code like this:
before do
class RoadrunnerEmailAlert
def async
end
end
end
But that's so contrived, it certainly isn't right... advice appreciated!
In case this helps other people... this is a very comprehensive answer.
Context & design notes
The async framework is Sucker Punch gem
(http://brandonhilkert.com/blog/why-i-wrote-the-sucker-punch-gem/).
Back then, this was the easiest thing for me to use after looking at
Delayed Job, Sidekick, etc
Basically it works like this: in Controller reference a Job that then references anything else (in my case, some POROs)
If I were really rigidly testing, I'd want to test that A) the Controller calls the Job appropriately and passes the right parameters, and B) the Job calls the appropriate POROs and passes the right parameters. But instead, I just tested that the Controller calls the appropriate POROs and passes the right parameters, i.e., the Jobs are already working.
Controller code
#order.save
RoadrunnerEmailAlert.new.async.perform(#order.id, true)
CalendarInvite.new.async.perform(#order.id)
RoadrunnerTwilioAlert.new.async.perform(#order.id)
Job code
# app/jobs/roadrunner_email_alert.rb
class RoadrunnerEmailAlert
include SuckerPunch::Job
def perform(order_id, require_tos)
ActiveRecord::Base.connection_pool.with_connection do
OrderMailer.success_email(order_id, require_tos).deliver
end
end
end
# app/jobs/calendar_invite.rb
class CalendarInvite
include SuckerPunch::Job
def perform(order_id)
ActiveRecord::Base.connection_pool.with_connection do
CreateCalendar.new(order_id).perform
end
end
end
# app/jobs/roadrunner_twilio_alert.rb
class RoadrunnerTwilioAlert
include SuckerPunch::Job
def perform(order_id)
ActiveRecord::Base.connection_pool.with_connection do
CreateAlert.new(order_id).perform
end
end
end
Test code
The really big thing here that I don't know why I keep forgetting (but only in testing) is class vs. instance of class. For the POROs, since I'm instantiating the object, I needed to test 2 different "layers" (first that the object is instantiated appropriately, second that the instantiated object is acted upon appropriately).
require 'sucker_punch/testing/inline'
describe "Controller code" do
before do
OrderMailer.any_instance.stub(:success_email)
mock_calendar = CreateCalendar.new(1)
CreateCalendar.stub(:new).and_return(mock_calendar)
CreateCalendar.any_instance.stub(:perform)
mock_alert = CreateAlert.new(1)
CreateAlert.stub(:new).and_return(mock_alert)
CreateAlert.any_instance.stub(:perform)
end
it "should call appropriate async jobs" do
expect_any_instance_of(OrderMailer).to receive(:success_email).with(1, true)
expect(CreateCalendar).to receive(:new).with(1)
expect_any_instance_of(CreateCalendar).to receive(:perform)
expect(CreateAlert).to receive(:new).with(1)
expect_any_instance_of(CreateAlert).to receive(:perform)
post :create
end
end
I have a PlantTree job that calls a PlantTree service object. I would like to test the job to ascertain that it instantiates the PlantTree service with a tree argument and calls the call method.
I'm not interested in what the service does or the result. It has its own tests, and I don't want to repeat those tests for the job.
# app/jobs/plant_tree_job.rb
class PlantTreeJob < ActiveJob::Base
def perform(tree)
PlantTree.new(tree).call
end
end
# app/services/plant_tree.rb
class PlantTree
def initialize(tree)
#tree = tree
end
def call
# Do stuff that plants the tree
end
end
As you can see, the PlantTree class is hard coded in the perform method of the job. So I can't fake it and pass it in as a dependency. Is there a way I can fake it for the life time of the perform method? Something like...
class PlantTreeJobTest < ActiveJob::TestCase
setup do
#tree = create(:tree)
end
test "instantiates PlantTree service with `#tree` and calls `call`" do
# Expectation 1: PlantTree will receive `new` with `#tree`
# Expectation 2: PlatTree will receive `call`
PlantTreeJob.perform_now(#tree)
# Verify that expections 1 and 2 happened.
end
end
I'm using Rails' default stack, which uses MiniTest. I know this can be done with Rspec, but I'm only interested in MiniTest. If it's not possible to do this with MiniTest only, or the default Rails stack, I'm open to using an external library.
You should be able to do something like
mock= MiniTest::Mock.new
mock.expect(:call, some_return_value)
PlantTree.stub(:new, -> (t) { assert_equal(tree,t); mock) do
PlantTreeJob.perform_now(#tree)
end
mock.verify
This stubs the new method on PlantTree, checks the argument to tree and then returns a mock instead of a PlantTree instance. That mock further verifies that call was called.
Not sure how to write this in Minitest, but you can use a mock (RSpec syntax here):
expect(PlantTree).to receive(:new).with(tree)
expect_any_instance_of(PlantTree).to receive(:call)
# NOTE: either of the above mocks (which are expectations)
# will fail if more than 1 instance receives the method you've mocked -
# that is, PlantTree#new and PlantTree#call
# In RSpec, you can also write this using `receive_message_chain`:
# expect(PlantTree).to receive_message_chain(:new, :call)
job = PlantTreeJob.new(#tree)
job.perform
This test will fail unless your PlantTree service object (1) gets instantiated via #new, and (2) gets #call'ed.
Disclaimer: this might not be 100% functional but this should be the right idea, assuming I've read OP's Q correctly.
plant_tree_mock= MiniTest::Mock.new
dummy = Object.new
tree = Object.new
plant_tree_mock.expect(:call, dummy, [tree])
PlantTree.stub(:new, plant_tree_mock) do
PlantTreeJob.perform_now(tree)
end
assert plant_tree_mock.verify
I'm using resque to do some (long time) job. And I have a few classes with the same mixed-in module for queuing. Class Service substitutes in tests, that's why it standalone and (maybe too much) complicated. So the story is when I call
Campaign.perform(user_id)
directly, everything works fine, but when I try to use queue:
Resque.enqueue(Campaign, user_id)
Job created, but seems like do nothing. At least, nothing saves into the database. Which is main task of Campaign class. I can see in resque-web-interface that jobs creates and finished, and finished (to fast, almost just after create) but no result.
I'm new in Resque and not really sure it calls it all (confused how to debug it).
Does anybody have similar problem? thanks for any help.
Module:
module Synchronisable
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def perform(user_id)
save_objects("#{self.name}::Service".constantize.get_objects(user_id))
end
protected
def save_objects(objects)
raise ArgumentError "should be implemented"
end
end
class Service
def self.get_objects(user)
raise ArgumentError "should be implemented"
end
end
end
One of the classes:
class Campaign < ActiveRecord::Base
include Synchronisable
#queue = :app
class << self
protected
def save_objects(objects)
#some stuff to save objects
end
end
class Service
def self.get_objects(user_id)
#some stuff to get objects
end
end
end
This is a very old question so not sure how rails folder structure was back then but I had the same problem and issue was with inheritance. Seems if you are using Resque your job classes shouldn't inherit from ApplicationJob.
so if your code was like this in (app/jobs/campaign_job.rb):
class Campaign < ApplicationJob
#queue = :a_job_queue
def self.perform
#some background job
end
end
then remove the inheritance i.e "< ApplicationJob"
These jobs are almost certainly failing, due to an Exception. What is resque-web showing you on the Failures tab? You can also get this from the Rails console with:
Resque.info
or
Resque::Failure.all(0)
You should run your worker like this:
nohup QUEUE=* rake resque:work & &> log/resque_worker_QUEUE.log
This will output everything you debug to "log/resque_worker_QUEUE.log" and you will be able to find out what's wrong with your Campaign class.
Try this:
env TERM_CHILD=1 COUNT=2 "QUEUE=*" bundle exec rake resque:workers