2

I'm trying to test to make sure the notification mailer is not sending after an invalid record but I keep getting below error before test can complete

"ActiveRecord::RecordInvalid:

      it 'does not call send_email_notification' do
        expect(NotificationMailer).not_to receive(:user_notification)
        FactoryGirl.create(:invalid_user, shop: shop)
      end

How can I test this properly?

EDIT: here's the code where the mail gets sent:

after_create :send_email_notification


  private


  def send_email_notification
    if self.shop.email_notifications
        NotificationMailer.user_notification(self).deliver_now
    end
  end
end
Jackson Cunningham
  • 4,973
  • 3
  • 30
  • 80
  • 1
    Since an exception has been fired that means that there is a validation issue, which is the expected behavior in your case since your trying to save an invalid_user. That by itself ensures that no mail has been sent. The question is where in your code have you stated the send email? is after_save, or create? – moeabdol Sep 26 '15 at 04:05
  • After create . So how would I write a spec for this ? Expect invalid user not to be valid? – Jackson Cunningham Sep 26 '15 at 04:07
  • 1
    Can you share the part of the code where you do the actual mail sending. – moeabdol Sep 26 '15 at 04:09

2 Answers2

2
it 'does not send notification email when user is invalid' do
  expect(NotificationMailer).not_to receive(:user_notification)
  post :create, user: attributes_for(:invalid_user)
end

So, what this is doing is set you're expectation just the way you did, and then post to the user_controller create method the invalid_user attributes.

Of course, the post shouldn't be allowed to create the record if you have set your validations correctly in the user model, and subsequently not call NotificationMailer.user_notification.

Note that attributes_for is another FactoryGirl method that you can use to arrange and pass your factory attributes as controller params.

Now! why does it not work with your original approach? It is is because FactoryGirl is complaining that it cannot create the record, and that is absolutely logical since you're trying to create an invalid user. The failing error has nothing to do with testing your email notification, but rather with the way you setup your Factory.

Final note! If you run the test and it complains:

"NoMethodError: undefined method `post' for #<RSpec::ExampleGroups"

It probably means that your spec file is not located under spec/controllers.

post, create, patch, delete methods are part of the RSpec::Rails::ControllerExampleGroup

To solve this please refer to the following Stackoverflow answer

Hope this helps.

Community
  • 1
  • 1
moeabdol
  • 4,779
  • 6
  • 44
  • 43
0

Below is some code I used to test your use case: you can copy and paste it into a file and run rspec on it. I hope that the assumptions I made about the parts of your Rails app you didn't disclose aren't too far off the mark.

require 'active_record'
require 'factory_girl'
require 'rspec'

ActiveRecord::Base.establish_connection(
  adapter: 'sqlite3', database: ':memory:'
)

class User < ActiveRecord::Base
  has_one :shop
  validates :email, presence: true
  after_create :send_notification

  private

  def send_notification
    if shop.email_notifications
      NotificationMailer.user_notification(self).deliver_now
    end
  end
end

class Shop < ActiveRecord::Base
  belongs_to :user
end

ActiveRecord::Schema.define do
  create_table :users do |t|
    t.string :email
  end

  create_table :shops do |t|
    t.boolean :email_notifications
    t.belongs_to :user
  end
end

FactoryGirl.define do
  factory :user do
    email "test@example.com"
    shop

    factory :invalid_user do
      email nil
    end
  end

  factory :shop do
    email_notifications true
  end
end

RSpec.describe User do
  context 'on save' do
    let(:mailer) { double('mailer') }

    before do
      # You probably won't need this stub_const since the class will exist
      # in your app
      stub_const('NotificationMailer', Class.new)
      allow(NotificationMailer).to \
        receive(:user_notification).with(user).and_return(mailer)
    end

    context 'when user is valid' do
      let(:user) { FactoryGirl.build(:user) }

      it 'calls to send email notifications' do
        expect(mailer).to receive(:deliver_now)
        user.save
      end
    end

    context 'when user is invalid' do
      let(:user) { FactoryGirl.build(:invalid_user) }

      it 'does not call to send email notifications' do
        expect(mailer).to_not receive(:deliver_now)
        user.save
      end
    end
  end
end

Since you've got an external dependency in your callback (the callout to the separate class NotificationMailer), you may need to stub out messages to that dependency in order to make the test pass, otherwise you could get nil values returned when you likely don't expect them (see this blog post for more information).

Just an opinion, but you might even do future-you a favour if you only use callbacks when there are no external dependencies in them and the logic only refers to state internal to the object (User in this case). The change you would make would be something like moving the NotificationMailer.user_notification(self).deliver_now call out of the User model callback and into the controller where (I assume) you're making a call to save the user. The extraction might look something like:

def create
  @user = User.new(user_params)
  if @user.save
    NotificationMailer.user_notification(@user).deliver_now
    # do other stuff, render, redirect etc
  else
    # do something else
  end
end
Paul Fioravanti
  • 16,423
  • 7
  • 71
  • 122