Rspec test retry_on - ruby-on-rails

i have a job like this
class CrawlSsbHistory < BaseJob
retry_on(Ssb::SessionExpired) do
Ssb::Login.call
end
def perform
response = Ssb::Client.get_data
SsbHistory.last.destroy
end
end
and i have test like this
it "retries the job if session error" do
allow(Ssb::Client).to receive(:get_data).and_raise(Ssb::SessionExpired)
allow(Ssb::Login).to receive(:call)
described_class.perform_now # it is CrawlSsbHistory
expect(Ssb::Login).to have_received(:call)
end
CrawlSsbHistory is a job to crawl some data. it call Ssb::Client.get_data to get the data.
Inside Ssb::Client.get_data i raise Ssb::SessionExpired if the session expired. so then i can capture the raised error on the job using retry_on. Then if it is happened i want to try the job.
but i got error like this
(Ssb::Login (class)).call(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Does the job no call retry_on? or do i test it wrong? how to make a rspec to test that retry_on is working and the Ssb::Login.call is called?

retry_on does not call the block immediately, on each retry. Only when attempts are exhausted. Otherwise, it just reschedules the job.
From the documentation:
You can also pass a block that'll be invoked if the retry attempts fail for custom logic rather than letting the exception bubble up. This block is yielded with the job instance as the first and the error instance as the second parameter.

I assume that you are testing ActiveJob, i guess retry_on will enqueue job instead of perform immediately, so you could try setup job queue using ActiveJob::TestHelper
your test case should be:
require 'rails_helper'
RSpec.describe CrawlSsbJob, type: :job do
include ActiveJob::TestHelper
before(:all) do
ActiveJob::Base.queue_adapter = :test
end
it "retries the job if session error" do
allow(Ssb::Client).to receive(:get_data).and_raise(Ssb::SessionExpired)
expect(Ssb::Login).to receive(:call)
# enqueue job
perform_enqueued_jobs {
described_class.perform_later # or perform_now
end
end
end

Related

How to test the rescue block in my model callback in RSpec?

I have an after_save method in my model, to call some background task. Since this task is depending on another server being up, I thought it was a nice idea to create a fallback to perform the task on the main thread when the call to the other server fails.
This is basically the callback:
def send_email
MyJob.perform_async(self.id)
rescue Errno::ECONNREFUSED
MyJob.new.perform(self.id)
end
Now I'd like to test this bahavior. I tried by mocking the MyJob and raising the exception on the perfrom_async method. But how do I test that perform is being called on the instance?
I already tried:
it "should use fallback to send e-mail after create when faktory is down" do
job = class_double("MyJob").
as_stubbed_const(:transfer_nested_constants => true)
allow(job).to receive(:perform_async) { raise Errno::ECONNREFUSED }
expect_any_instance_of(job).to receive(:perform)
company_opt_out
end
Thanks
no need to stub MyJob class
it "should use fallback to send e-mail after create when faktory is down" do
allow(MyJob).to receive(:perform_async).and_raise(Errno::ECONNREFUSED)
expect_any_instance_of(MyJob).to receive(:perform)
company_opt_out
end
but make sure your company_opt_out calls send_email method
Try to not overload the it block too much when you write the test as they become hard to read when you or another developer comes back to it later.
let(:job_instance) { instance_double(MyJob, perform: "Performing now") }
before do
allow(MyJob).to receive(:perform_async).and_return("Performing later")
allow(MyJob).to receive(:new).and_return(job_instance)
end
it "performs the job later" do
expect(send_email).to eq("Performing later")
end
context "when a network error is raised" do
before { allow(MyJob).to receive(:perform_async).and_raise(Errno::ECONNREFUSED) }
it "performs the job now" do
expect(send_email).to eq("Performing now")
end
end

why doesn't my delayed job work more than once when being triggered from a rails server?

given the delayed job worker,
class UserCommentsListWorker
attr_accessor :opts
def initialize opts = {}
#opts = opts
end
def perform
UserCommentsList.new(#opts)
end
def before job
p 'before hook', job
end
def after job
p 'after hook', job
end
def success job
p 'success hook', job
end
def error job, exception
p '4', exception
end
def failure job
p '5', job
end
def enqueue job
p '-1', job
end
end
When I run Delayed::Job.enqueue UserCommentsListWorker.new(client: client) from a rails console, I can get repeated sequences of print statements and a proper delayed job lifecyle even hooks to print including the feedback from the worker that the job was a success.
Including the same call to run the worker via a standard rails controller endpoint like;
include OctoHelper
include QueryHelper
include ObjHelper
include StructuralHelper
class CommentsController < ApplicationController
before_filter :authenticate_user!
def index
if params['updateCache'] == 'true'
client = build_octoclient current_user.octo_token
Delayed::Job.enqueue UserCommentsListWorker.new(client: client)
end
end
end
I'm noticing that the worker will run and created the delayed job, but none of the hooks get called and the worker nevers logs the job as completed.
Notice the screenshot,
Jobs 73,75,76 were all triggered via a roundtrip to the above referenced endpoint while job 74 was triggered via the rails console, what is wrong with my setup and/or what am I failing to notice in this process? I will stress that the first time the webserver hits the above controller endpoint, the job queues and runs properly but all subsequent instances where the job should run properly appear to be doing nothing and giving me no feedback in the process.
i would also highlight that i'm never seeing the failure, error or enqueue hooks run.
thanks :)
The long and the short of the answer to this problem was that if you notice, i was attempting to store a client object in the delayed job notification which was causing problems, so therefore, don't store complex objects in the job, just work with basic data ids 1 or strings foo or booleans true etc. capisce?

How can I overriding class with proc and yield in test on rails?

I have below classes(only for example),
class Background
def self.add_thread(&blcok)
Thread.new do
yield
ActiveRecord::Base.connection.close
end
end
end
class Email
def send_email_in_other_thread
Background.add_thread do
send_email
end
end
def send_email
UserMailer.greeting_email.deliver_now
end
end
And below codes are for tests,
class EmailTest < ActiveSupport::TestCase
class Background
def self.add_thread(&block)
yield
end
end
test 'should send email' do
assert_difference 'ActionMailer::Base.deliveries.size', 1 do
send_email_in_other_thread
end
end
end
But this test fails, "ActionMailer::Base.deliveries.size" didn't change by 1.
And 1 in about 20 times success.
I think it is because of the modified Background class. Maybe overriding in test doesn't work or yield proc is not executed instantly but in delayed.
I tried 'block.call' instead of yield, but the result is same.
How can I make this test always be success?
This looks like a classic race condition. Thread.new returns as soon as the thread is spawned, not when it's work is completed.
Because your main thread doesn't halt execution, most of the time your assertion is run before the mail has been sent.
You could use the join method to wait for the sending thread to finish execution before returning, but then it would essentially be equivalent to a single thread again, as it blocks the calling (main) thread until work is done.
Thread.new do
yield
ActiveRecord::Base.connection.close
end.join
There are already some great gems, however, for dealing with background jobs in Rails. Check out SideKiq and Resque for example.

RSpec: Testing that a mailer is called exactly once

I have a Mailer that looks something like this:
class EventMailer < BaseMailer
def event_added(user_id, event_id)
# do stuff and email user about the event
end
end
I'm calling this EventMailer like this from inside the Event class:
class Event < Task
def notify_by_email(user)
EmailLog.send_once(user.id, id) do
EventMailer.delay(queue: 'mailer').event_added(user.id, id)
end
end
end
where EmailLog is a class that logs sent emails. .delay is added by Sidekiq.
But when I try to test that #notify_by_email is called only once per event and user, my spec fails:
1) Event#notify_by_email only sends once per user
Failure/Error: expect(EventMailer).to receive(:event_added).once
(<EventMailer (class)>).event_added(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
The spec looks like:
let(:owner) { User.make! }
let(:product) { Product.make! }
let(:event) { Task.make!(user: owner, product: product) }
describe '#notify_by_email' do
before do
EventMailer.stub(:delay).and_return(EventMailer)
end
it 'only sends once per user' do
event.notify_by_email(owner)
event.notify_by_email(owner)
expect(EventMailer).to receive(:event_added).once
end
end
Any insights into why this spec is failing and how I can fix it? Strangely, if I put a puts statement inside the block that's passed to EmailLog.send_once, it prints only once, the spec still reports that EventMailer.event_added wasn't called.
Your expectation should be declared before the code you're testing. Using expect(...).to receive(...) basically means "this message should be received between now and the end of this spec". Because the expectation is the last line of your spec, it fails.
Try moving it before and you should be good to go:
it 'only sends once per user' do
expect(EventMailer).to receive(:event_added).once
event.notify_by_email(owner)
event.notify_by_email(owner)
end

Setting expectation on resque .perform method. Task is enqueued in a Callback

So based on my understanding, I beleive when you do
Resque.inline = Rails.env.test?
Your resque tasks will run synchronously. I am writing a test on resque task that gets enqueue during an after_commit callback.
after_commit :enqueue_several_jobs
#class PingsEvent < ActiveRecord::Base
...
def enqueue_several_jobs
Resque.enqueue(PingFacebook, self.id)
Resque.enqueue(PingTwitter, self.id)
Resque.enqueue(PingPinterest, self.id)
end
In the .perform methd of my Resque task class, I am doing a Rails.logger.info and in my test, I am doing something like
..
Rails.logger.should_receive(:info).with("PingFacebook sent with id #{dummy_event.id}")
PingsEvent.create(params)
And I have the same test for PingTwitter and PingPinterest.
I am getting failure on the 2nd and third expectation because it seems like the tests actually finish before all the resque jobs get run. Only the first test actually passes. RSpec then throws a MockExpectationError telling me that Rails.logger did not receive .info for the other two tests. Anyone has had experience with this before?
EDIT
Someone mentioned that should_receive acts like a mock and that I should do .exactly(n).times instead. Sorry for not making this clear earlier, but I have my expectations in different it blocks and I don't think a should_receive in one it block will mock it for the next it block? Let me know if i'm wrong about this.
class A
def bar(arg)
end
def foo
bar("baz")
bar("quux")
end
end
describe "A" do
let(:a) { A.new }
it "Example 1" do
a.should_receive(:bar).with("baz")
a.foo # fails 'undefined method bar'
end
it "Example 2" do
a.should_receive(:bar).with("quux")
a.foo # fails 'received :bar with unexpected arguments
end
it "Example 3" do
a.should_receive(:bar).with("baz")
a.should_receive(:bar).with("quux")
a.foo # passes
end
it "Example 4" do
a.should_receive(:bar).with(any_args()).once
a.should_receive(:bar).with("quux")
a.foo # passes
end
end
Like a stub, a message expectation replaces the implementation of the method. After the expectation is fulfilled, the object will not respond to the method call again -- this results in 'undefined method' (as in Example 1).
Example 2 shows what happens when the expectation fails because the argument is incorrect.
Example 3 shows how to stub multiple invocations of the same method -- stub out each call with the correct arguments in the order they are received.
Example 4 shows that you can reduce this coupling somewhat with the any_args() helper.
Using should_receive behaves like a mock. Having multiple expectations on the same object with different arguments won't work. If you change the expectation to Rails.logger.should_receive(:info).exactly(3).times your spec will probably past.
All that said, you may want to assert something more pertinent than what is being logged for these specs, and then you could have multiple targeted expectations.
The Rails.logger does not get torn down between specs, so it doesn't matter if the expectations are in different examples. Spitting out the logger's object id for two separate examples illustrates this:
it 'does not tear down rails logger' do
puts Rails.logger.object_id # 70362221063740
end
it 'really does not' do
puts Rails.logger.object_id # 70362221063740
end

Resources