You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Rspec和factory_bot测试邮件器时遇发送数量不符错误求助

Rspec + FactoryBot 邮件器测试报错:预期邮件发送数变化1,实际变化2

我是测试新手,使用Rspec和factory_bot测试邮件器时遇到问题,运行测试时出现错误:

expected ActionMailer::Base.deliveries.size to have changed by 1, but was changed by 2

邮件由User模型的after_create钩子触发,实际代码运行正常但测试失败,相关代码如下:


相关代码

verify_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 配置

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

已在environment/test.rb中添加config.action_mailer.delivery_method = :test配置。


问题原因

测试中邮件发送数翻倍的核心原因:

  • 执行create(:user)时,User模型的after_create钩子会调用send_user_otp方法,自动发送1封验证邮件。
  • 测试代码里又手动调用VerifyMailer.verification_email(user).deliver_now,再次发送1封邮件。
  • 两次发送导致ActionMailer::Base.deliveries的数量变化了2,和预期的1不符。

解决方法

有两种常见修复方式,可根据测试需求选择:

方式1:跳过User的after_create钩子(单独验证邮件器)

如果测试仅需验证邮件器的内容格式,不需要触发模型的业务逻辑,可以在创建用户时跳过回调:

方案1-1:直接跳过所有回调

修改verify_mailer_spec.rb中的user定义:

let(:user) { create(:user, without_protection: true) }

方案1-2:精准跳过指定回调

先给FactoryBot添加一个trait:

FactoryBot.define do
  factory :user do
    # ... 原有配置 ...
    trait :skip_verification_email do
      after(:build) do |user|
        user.skip_callback(:create, :after, :send_user_otp)
      end
    end
  end
end

然后在测试中使用该trait:

let(:user) { create(:user, :skip_verification_email) }

方式2:利用钩子触发的邮件进行验证(贴合业务场景)

如果测试目的是验证"用户创建后邮件是否正确发送",可以直接检查钩子触发的邮件,无需手动调用邮件器:

修改verify_mailer_spec.rb:

RSpec.describe VerifyMailer, type: :mailer do
  before do
    ActionMailer::Base.deliveries.clear # 清空历史邮件记录,避免干扰当前测试
  end

  it 'sends correct verification email after user creation' do
    user = create(:user)
    mail = ActionMailer::Base.deliveries.last

    expect(mail.to).to eq([user.email])
    expect(mail.subject).to eq('verification code')
    expect(mail.from).to eq(['mutebigod10@gmail.com'])
  end
end

额外建议

  • 所有邮件测试前,建议清空ActionMailer::Base.deliveries,避免之前的测试用例残留数据影响结果。
  • 如果后续改用deliver_later异步发送邮件,需要在测试中加入perform_enqueued_jobs来触发任务(当前用deliver_now无需此操作)。

内容的提问来源于stack exchange,提问作者mutebi godfrey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 18:54:54