RSpec: Testing that a mailer is called exactly once - ruby-on-rails

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

Related

how to check if a method is called or not in rspec

as i try to check internally my method was calling or not in rspec but it got the following errors
context "#Meeting_schedule" do
let(:meeting_schedule) { FactoryGirl.create(:meeting_schedule,:time=>"morning",:schedule_name=>"planned_meet", :schedule_info=>[{ "from"=>"00:00", "to"=>"00:01"}]) }
it "if the same schedule was created again dont save it again" do
schedule.save
params = {:time=>"morning",:schedule_name=>"planned_meet", :schedule_info=>[{ "from"=>"00:00", "to"=>"00:01"}]}
meeting_schedule.create_or_update_meeting_schedule(params)
expect(meeting_schedule).to receive(:updating_the_user)
end
end
i got the following error
Failure/Error: expect(meeting_schedule.create_or_update_meeting_schedule(params)).to receive(:updating_the_user)
(#<Meeting_schedule:0x0055dbaf0da710>).updating_the_user(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/models/meeting_schedule_spec.rb:122:in `block (4 levels)
so what was wrong in my code?
my method
def create_or_update_meeting_schedule(params)
self.attributes = params
if self.changed and self.save
updating_the_user
end
self
end
can anyone help me out
Mocks must always be setup before the method under test is called as there is no reliable way to test if a normal method was called in Ruby. There are two ways of doing this in RSpec.
The first is using expect(...).to receive(...) which must be done before the method is called - this detaches the method and replaces it with a mock that wraps the original method.
The test will fail if the method is not called in the example.
The second is by using spies. You can either replace an entire object with a spy:
RSpec.describe "have_received" do
it "passes when the message has been received" do
invitation = spy('invitation')
invitation.deliver
expect(invitation).to have_received(:deliver)
end
end
This "spy object" will keep track of any method you call on it.
You can also spy on a single method:
class Invitation
def self.deliver; end
end
RSpec.describe "have_received" do
it "passes when the expectation is met" do
allow(Invitation).to receive(:deliver)
Invitation.deliver
expect(Invitation).to have_received(:deliver)
end
end
Spies are very useful in the case when you want to mock the method in the test setup - for example in the before block.
class Invitation
def self.deliver; end
end
RSpec.describe "have_received" do
before do
allow(Invitation).to receive(:deliver)
end
it "passes when the expectation is met" do
Invitation.deliver
expect(Invitation).to have_received(:deliver)
end
end

Check if line got executed with Rspec

I have a worker that sends email to users when new feedback pops in. I wanted to allow user to not agree to that (with Shih Tzu flags). Question is: how can I test (with Rspec) if the FedbackMailer.new_feedback line gets executed?
account.users.each do |user|
return if (user.no_notifications || user.just_summary)
FeedbackMailer.new_feedback(account.id, feedback_id, user.id).deliver_later
end
You can use rspec-mocks.
mailer = instance_double
allow(FeedbackMailer).to receive(:new_feedback).with(account_id, feedback_id, user_id).and_return(mailer)
allow(mailer).to receive(:deliver_later)
## do stuff ##
expect(mailer).to have_received(:deliver_later)
You can also ignore the .with if you haven't the arguments to pass in that moment.
Another solution is to set a config config.action_mailer.delivery_method = :test and check if the delivery counts have changed.
expect {
## code that deliver the email
}.to change { ActionMailer::Base.deliveries.count }.by(1)
Let's say your logic is encapsulated in following method in class MyClass
class MyClass
def my_method
account.users.each do |user|
return if (user.no_notifications || user.just_summary)
FeedbackMailer.new_feedback(account.id, feedback_id, user.id).deliver_later
end
end
end
RSpec.describe MyClass, type: :model do
context "#my_method" do
it "should send new feedback" do
user_obj = create_user
expect(user_obj.no_notifications).to be_falsey
#OR
#expect(user_obj.just_summary).to be_falsey
account_obj = create_account
account_obj.users << user_obj
expect(account_obj.users).to include(user_obj)
expect(FeedbackMailer).to receive(:new).with(account_obj.id, feedback_id, user_obj.id)
# OR in case you don't have feedback_id then you can use
# expect(FeedbackMailer).to receive(:new).with(account_obj.id, kind_of(Numeric), user_obj.id)
# You should also setup expectation here that `FeedbackMailer` gets enqueued to ensure
# that your method also gets invoked and the job also gets enqueued.
subject.my_method
end
end
end
Hope that helps. Thanks.

Rspec: How to test that on every each loop class method is called?

I am currently trying to test a portion of code in my rails worker class as shown below(simplified version);
class SenderWorker
include Sidekiq::Worker
sidekiq_options :retry => 5
def perform(current_user_guid)
Rails.logger.info "Starting for user_guid: #{current_user_guid}"
user = User.find_by!(guid: current_user_guid)
team = Team.find_by!(uuid: user.team.uuid)
profiles = team.profiles
profiles.each do |profile|
SenderClass.new(profile,
user).send(User::RECALL_USER)
end
Rails.logger.info "Finishing for user_guid: #{current_user_guid}"
end
end
The tests that I have written are these and they are passing;
context 'when something occurs' do
it 'should send' do
sender = double("sender")
allow(SenderClass).to receive(:new).with(user_profile, current_user) { sender }
expect(sender).to receive(:send)
expect(Rails.logger).to receive(:info).exactly(2).times
worker.perform(user.guid)
end
end
However, I am not testing for all calls. Is there a way to ensure that I test for everything called in the each do loop. Thank you in advance.
You can test that :send is received an expected amount of times.
But I'd suggest you to simplify the test by using a class method to encapsulate those chained methods. Something like:
def self.profile_send(profile, user)
new(profile, user).send(User::RECALL_USER)
end
Then:
def perform(current_user_guid)
Rails.logger.info "Starting for user_guid: #{current_user_guid}"
user = User.find_by!(guid: current_user_guid)
team = Team.find_by!(uuid: user.team.uuid)
profiles = team.profiles
profiles.each do |profile|
SenderClass.profile_send(profile, user)
end
Rails.logger.info "Finishing for user_guid: #{current_user_guid}"
end
And now you can test that SenderClass receives :send_profile X times.
You can then add a test for SenderClass.send_profile if you really want to test the new and the send method calls, but then you can test that once, outside a loop and both tests will cover what you want.

How to test whether Pusher channel is triggered in ActiveRecord model

I have the following method in a model:
def trigger_events_updated_push_event
Pusher['events'].trigger('events_updated', {})
end
I have the following spec:
describe '#trigger_events_updated_push_event' do
it 'should send message to Pusher' do
Pusher['events'].should_receive(:trigger).with('events_updated', {})
subject.send(:trigger_events_updated_push_event)
end
end
Which produces this error:
Failure/Error: Unable to find matching line from backtrace
(#<Pusher::Channel:0x007ff16f18ae58>).trigger("events_updated", {})
expected: 1 time
received: 0 times
What am I failing to do?
At first your code needs improvement. You hardcoded another Class/Constant in this class that makes it tightly coupled.
Refactor at first
def trigger_events_updated_push_event(event=Pusher['event'])
event.trigger('events_updated', {})
end
# This way you allow other kinds of event to be injected.
Then test.
it "fires event" do
event = double
event.stub(:trigger).with(anything)
event.should_receive(:trigger).with('events_updated', {})
subject.trigger_events_updated_push_event
end
# Your test is only about this Class and does not depend on Pusher at all.

Testing gems within a Rails App

I'm attempting to test that my service is calling Anemone.crawl correctly. I have the following code:
spider_service.rb
class SpiderService < BaseService
require 'anemone'
attr_accessor :url
def initialize(url)
self.url = url
end
def crawl_site
Anemone.crawl(url) do |anemone|
end
end
end
spider_service_spec.rb
require 'spec_helper'
require 'anemone'
describe SpiderService do
describe "initialize" do
let(:url) { mock("url") }
subject { SpiderService.new(url) }
it "should store the url in an instance variable" do
subject.url.should == url
end
end
describe "#crawl_site" do
let(:spider_service) { mock("spider service") }
let(:url) { mock("url") }
before do
SpiderService.stub(:new).and_return(spider_service)
spider_service.stub(:crawl_site)
Anemone.stub(:crawl).with(url)
end
subject { spider_service.crawl_site }
it "should call Anemone.crawl with the url" do
Anemone.should_receive(:crawl).with(url)
subject
end
end
end
And here's the error that I'm getting, and can't understand, since I can call the service in the Rails console and I get back data from Anemone when I provide a valid URL:
Failures:
1) SpiderService#crawl_site should call Anemone.crawl with the url
Failure/Error: Anemone.should_receive(:crawl).with(url)
(Anemone).crawl(#<RSpec::Mocks::Mock:0x82bdd454 #name="url">)
expected: 1 time
received: 0 times
# ./spec/services/spider_service_spec.rb:28
Please tell me I've forgotten something silly (I can blame lack of coffee then, instead of general incompetence!)
Thank you for your time,
Gav
Your subject calls a method on the mock object that you're created (mock("spider_service")), not a real SpiderService object. You've also stubbed the call on the mock spider service to do nothing, so calling it in the subject will do nothing, hence why your test fails.
Also, you've stubbed new (although you never call it) on SpiderService to return a mock object. When you're testing SpiderService you'll want to have real instances of the class otherwise method calls will not behave as they would on a real instance of the class.
The following should achieve what you want:
describe "#crawl_site" do
let(:spider_service) { SpiderService.new(url) }
let(:url) { mock("url") }
before do
Anemone.stub(:crawl).with(url)
end
subject { spider_service.crawl_site }
it "should call Anemone.crawl with the url" do
Anemone.should_receive(:crawl).with(url)
subject
end
end
You might also want to move the require 'anenome' outside of the class definition so it is available elsewhere.

Resources