ruby-on-railsrspec-railsactionmailerrspec3ruby-on-rails-6.1

Testing mailers with Rspec and factory_bot


verfiy_mailer.rb

# frozen_string_literal: true

class VerifyMailer < ApplicationMailer
  def verification_email(user)
    @user = user
    mail(to: user.email, subject: 'verification code')
  end
end

verify_mailer_spec.rb

# frozen_string_literal: true

require 'rails_helper'
# include ActiveJob::TestHelper

RSpec.describe VerifyMailer, type: :mailer do
  let(:user) { create(:user) }
  let(:mail) { VerifyMailer.verification_email(user).deliver_now }

  it 'renders the receiver email' do
    expect(mail.to).to eq([user.email])
  end
  it 'renders the subject' do
    expect(mail.subject).to eq('verification code')
  end
  it 'renders the sender email' do
    expect(mail.from).to eq(['mutebigod10@gmail.com'])
  end
end

user.rb

# frozen_string_literal: true

class User < ApplicationRecord
  after_create :send_user_otp
  attr_writer :login

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable ,

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, authentication_keys: [:username]
  has_many :payments
  validates :email, :username, presence: true, uniqueness: true
  validates :username, format: { with: /\A[a-zA-Z0-9_.]*\z/,
                                 message: ' only allow letter, number, underscore and punctuation marks' }

  # Loggin with user_name
  def login
    @login || username
  end

  def send_user_otp
    unverify!
    otp = generate_codes
    update_column(:otp_code, otp)
    VerifyMailer.verification_email(self).deliver_now
    touch(:otp_sent_at)
  end

  def unverify!
    update_column(:verified, false)
  end

  def generate_codes
    loop do
      code = rand(0o00000..999_999).to_s
      break code unless code.length != 6
    end
  end
end

FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "person#{n}@example.com" }
    sequence(:username) { |n| "user#{n}" }
    password { '!Mutebi2' }
    password_confirmation { '!Mutebi2' }
  end
end

Solution

  • As in most cases, the computer is doing exactly what you told it to do:

      let(:user) { create(:user) }
      let(:mail) { VerifyMailer.verification_email(user).deliver_now }
    

    Creating the user with FactoryBot triggers any/all callbacks:

    class User < ApplicationRecord
      after_create :send_user_otp
    

    So create(:user) calls :send_user_otp after it's created.

    Then you send another email with VerifyMailer.verification_email(user).deliver_now

    Hence why ActionMailer::Base.deliveries.size changes by 2.


    In your VerifyMailer tests, you are testing the attributes of the email, so you don't need the user to actually be saved or the email to actually be sent.

    If you need an instance of the email, just remove deliver_now. And you can build instead of create the user:

    RSpec.describe VerifyMailer, type: :mailer do
      let(:user) { build(:user) }
      let(:mail) { VerifyMailer.verification_email(user) }
    
      it 'renders the receiver email' do
        expect(mail.to).to eq([user.email])
      end
      it 'renders the subject' do
        expect(mail.subject).to eq('verification code')
      end
      it 'renders the sender email' do
        expect(mail.from).to eq(['mutebigod10@gmail.com'])
      end
    end
    

    In your User tests, you can verify that creating a user also creates the email:

    RSpec.describe User, type: :model do
      let(:user) { build(:user) }
    
      describe "after create" do
        it "calls #send_user_otp" do
          expect(user).to receive(:send_user_otp)
        end
      end
    
      describe "#send_user_otp" do
        it "calls VerifyMailer.verification_email" do
          expect(VerifyMailer).to receive(:verification_email).with(user)
    
          user.send(:send_user_otp)
        end
      end
    end
    

    Note that I'm not actually testing ActionMailer::Base.deliveries.size at all. That's a service or request test, which should be based on a full implementation, e.g.:

    When this form is filled out:

    Your User model isn't responsible for VerifyMailer, ApplicationMailer and any other code that handles delver_now, it's only responsible for calling VerifyMailer and handing it the user instance.

    And VerifyMailer isn't responsible for handling delivery, it creates the email and passes it off to ActionMailer.

    Keep your tests focused on the responsibilities of your code, it will make them less brittle.

    If you change something in your ActionMailer setup, you don't want User tests to start to fail.