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
Related
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
I have a rails method which allows a user to submit a review and to counterparty, it sends an email using the delayed jobs.
def update_review
#review.add_review_content(review_params)
ReviewMailer.delay.review_posted(#review.product_owner, params[:id])
end
And I am trying to add a rspec test for this to check if the mailer is delivered properly and to whom. The delayed jobs run immediately after they are created on test because I want other jobs like the job to update the product owners overall rating to be completed immediately.
So the email does get fired but how can I add a test for it?
EDIT: Adding current tests
My current tests are:
describe 'PUT #update the review' do
let(:attr) do
{ rating: 3.0, raw_review: 'Some example review here' }
end
before(:each) do
#review = FactoryBot.create :review
put :update, id: #review.id, review: attr
end
it 'creates a job' do
ActiveJob::Base.queue_adapter = :test
expect {
AdminMailer.review_posted(#coach, #review.id).deliver_later
}.to have_enqueued_job
end
it { should respond_with 200 }
end
This does test that the mailer works properly but I want to test that it gets triggered properly in the method flow as well.
It sounds like what you want is to ensure that the update_review method enqueues a job to send the correct email to the correct recipient. Here's a simpler way to accomplish that:
describe 'PUT #update the review' do
let(:params) { { rating: rating, raw_review: raw_review } }
let(:rating) { 3.0 }
let(:raw_review) { 'Some example review here' }
let(:review) { FactoryBot.create(:review) }
let(:delayed_review_mailer) { instance_double(ReviewMailer) }
before do
# assuming this is how the controller finds the review...
allow(Review).to receive(:find).and_return(review)
# mock the method chain that enqueues the job to send the email
allow(ReviewMailer).to receive(:delay).and_return(delayed_review_mailer)
allow(delayed_review_mailer).to receive(:review_posted)
put :update, id: review.id review: params
end
it 'adds the review content to the review' do
review.reload
expect(review.rating).to eq(rating)
expect(review.raw_review).to eq(raw_review)
end
it 'sends a delayed email' do
expect(ReviewMailer).to have_received(:delay)
end
it 'sends a review posted email to the product owner' do
expect(delayed_review_mailer)
.to have_received(:review_posted)
.with(review.product_owner, review.id)
end
end
The reason I prefer this approach is that a) it could be done without touching the database at all (by swapping the factory for an instance double), and b) it doesn't try to test parts of Rails that were already tested by the folks who built Rails, like ActiveJob and ActionMailer. You can trust Rails' own unit tests for those classes.
I want to test that a class receives a class-method call in RSpec:
describe MyObject do
it "should create a new user" do
expect(User).to receive(:new)
MyObject.new.doit
end
end
class MyObject
def doit
u = User.new
u.save
end
end
The problem is that the expectation does not halt execution. It simply stubs the class method .doit and continues execution.
The effect of the expectation is to ensure that User.new returns nil. So when we get to the next line which is User.save it then fails because there is no user object to call .save on.
I would like execution to halt as soon as the RSpec expectation has been satisfied - how can I do that?
nb
This is just an illustrative example - while an expect to change would work for User.new, it's not this actual code that I need to test
There is a great method for this and_call_original:
expect(User).to receive(:new).and_call_original
based on your test description, you're testing that a record was created, in those cases I would suggest you to do this:
expect {
MyObject.new.doit
}.to change{User.count}
or if you want to make sure it only created one:
expect {
MyObject.new.doit
}.to change{User.count}.by(1)
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
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