ruby-on-railsrubyrspecmailer

Ruby on Rails - Testing Mailer as a callback in Model


I have this logic where I send a welcome email to the user when it is created.

User.rb

class User < ApplicationRecord
  # Callbacks
  after_create :send_registration_email

  private

  def send_registration_email
    UserConfirmationMailer.with(user: self).user_registration_email.deliver_later
  end 
end

I tried testing it in user_spec.rb:

describe "#save" do
 subject { create :valid_user }

 context "when user is created" do
   it "receives welcome email" do
     mail = double('Mail')
     expect(UserConfirmationMailer).to receive(:user_registration_email).with(user: subject).and_return(mail)
     expect(mail).to receive(:deliver_later)
   end
 end
end

But it is not working. I get this error:

Failure/Error: expect(UserConfirmationMailer).to receive(:user_registration_email).with(user: subject).and_return(mail)
     
       (UserConfirmationMailer (class)).user_registration_email({:user=>#<User id: 5, email: "factory10@test.io", created_at: "2023-02-18 01:09:34.878424000 +0000", ...878424000 +0000", jti: "2163d284-1349-4e48-8a2a-1b52b578921c", username: "jose_test10", icon_id: 5>})
           expected: 1 time with arguments: ({:user=>#<User id: 5, email: "factory10@test.io", created_at: "2023-02-18 01:09:34.878424000 +0000", ...878424000 +0000", jti: "2163d284-1349-4e48-8a2a-1b52b578921c", username: "jose_test10", icon_id: 5>})
           received: 0 times

Am I doing something wrong when testing the action of sending the email in a callback?

PD.

My environment/test.rb config is as follows:

config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
config.action_mailer.default_url_options = { :host => "http://localhost:3000" }

Even if I change config.active_job.queue_adapter to :test following this, I get the same error.

Another way I am trying to do it is like this:

expect { FactoryBot.create(:valid_user) }.to  have_enqueued_job(ActionMailer::MailDeliveryJob).with('UserConfirmationMailer', 'user_registration_email', 'deliver_now', subject)

But then subject and the user created in FactoryBot.create(:valid_user) is different..

Any ideas are welcome. Thank you!


Solution

  • The stubbing in the question doesn't work because it doesn't stub the chain of methods in the same order then they are called in the implementation.

    When you want to stub this method chain

    UserConfirmationMailer.with(user: self).user_registration_email.deliver_later
    

    then you have to first stub the with call, then the user_registration_email and last the deliver_later.

    describe '#save' do
      subject(:user) { build(:valid_user) }
    
      let(:parameterized_mailer) { instance_double('ActionMailer::Parameterized::Mailer') }
      let(:parameterized_message) { instance_double('ActionMailer::Parameterized::MessageDelivery') }
    
      before do 
        allow(UserConfirmationMailer).to receive(:with).and_return(parameterized_mailer)
        allow(parameterized_mailer).to receive(:user_registration_email).and_return(parameterized_message)
        allow(parameterized_message).to receive(:deliver_later)
      end
    
      context 'when user is created' do
        it 'sends a welcome email' do
          user.save!
    
          expect(UserConfirmationMailer).to have_received(:with).with(user: user)
          expect(parameterized_mailer).to have_received(:user_registration_email)
          expect(parameterized_message).to have_received(:deliver_later)
        end
      end
    end
    

    Note: I am not sure if using instance_double will work in this case, because the Parameterized uses method_missing internally. Although instance_double is usually preferred, you might need to use double instead.