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

Rails测试疑问:ActiveMailer发送后delivery_method.settings被清空?

问题:测试ActiveMailer动态SMTP设置时,发送后无法获取配置

我需要验证UserMailer是否按照租户指定的SMTP服务器发送邮件,在UserMailer中通过before_action钩子动态设置SMTP配置:

class UserMailer < ApplicationMailer
  before_action :set_user
  before_action :set_smtp_settings

  # ... 其他代码 ...

  def set_smtp_settings
    tenant_email_provider = @user.tenant.email_providers.where(provider_type: 'smtp').take
    tenant_smtp_settings = (tenant_email_provider&.settings || {}).with_indifferent_access
    
    current_settings = (message.delivery_method.settings || {} ).with_indifferent_access
    new_settings = current_settings.merge(tenant_smtp_settings)
    
    raise "No server address to send to" if new_settings[:address].nil?
    
    message.delivery_method.settings = new_settings
  end
end

单元测试代码如下:

test 'that the settings are according to a mailer' do
  expected_settings = email_provider.settings.with_indifferent_access

  message = @mailer.account_activation_confirm
  sent_message = message.deliver_now

  actual_settings = sent_message.delivery_method.settings.with_indifferent_access

  intersecting_keys = expected_settings.keys.map(&:to_sym) | actual_settings.keys
  assert intersecting_keys.include?(:address)

  intersecting_keys.each do |setting|
    expected = expected_settings[setting].to_s
    actual = actual_settings[setting].to_s
    assert_equal(expected, actual, "smtp value of setting '#{setting}' do not match (#{expected} != #{actual})")
  end
end

测试失败,报错信息:

Failure:
DeliverSettingsTest#test_that_the_settings_are_according_to_a_mailer [/opt/xxx-web/test/mailers/deliver_settings_test.rb:25]:
smtp value of setting 'address' do not match (mail.hello.com != ).
--- expected
+++ actual
@@ -1 +1,3 @@
-"mail.hello.com"
+# encoding: US-ASCII
+#    valid: true
+""

通过调试发现:

  • 在set_smtp_settings钩子中,message.delivery_method.settings已正确填充租户的SMTP配置:
    (ruby) message.delivery_method.settings
    {"address"=>"mail.hello.com",
     "port"=>25,
     "domain"=>"localhost.localdomain",
     "user_name"=>"prius",
     "password"=>"bunch_tr1k3s!",
     "authentication"=>nil,
     "enable_starttls"=>nil,
     "enable_starttls_auto"=>true,
     "openssl_verify_mode"=>nil,
     "ssl"=>nil,
     "tls"=>nil,
     "open_timeout"=>5,
     "read_timeout"=>5}
    
  • 但在测试代码中,sent_message.delivery_method.settings是空哈希:
    (ruby) actual_settings
    {}
    

疑问:是否是ActiveMailer库或测试桩在邮件发送后清空了delivery_method.settings的内容?


分析原因

确实是测试环境下ActiveMailer的默认行为导致的:

  1. 测试环境默认使用:test投递方式:测试模式中,ActiveMailer不会真正通过SMTP发送邮件,而是用ActionMailer::DeliveryMethods::Test这个桩实现,仅把邮件存入内存的ActionMailer::Base.deliveries数组,不会保留原SMTP投递方式的配置。
  2. 发送后返回的sent_message已切换为测试桩:调用deliver_now后,邮件对象的投递方式会被替换为测试用的实现,你之前设置的SMTP配置不会保留在返回的sent_message中。

解决方案

可以通过两种方式调整测试逻辑:

方案1:发送前直接验证邮件对象的配置

无需等待发送,直接在调用deliver_now前检查message的投递配置:

test 'that the settings are according to a mailer' do
  expected_settings = email_provider.settings.with_indifferent_access

  message = @mailer.account_activation_confirm
  # 发送前验证配置
  actual_settings = message.delivery_method.settings.with_indifferent_access

  intersecting_keys = expected_settings.keys.map(&:to_sym) | actual_settings.keys
  assert intersecting_keys.include?(:address)

  intersecting_keys.each do |setting|
    expected = expected_settings[setting].to_s
    actual = actual_settings[setting].to_s
    assert_equal(expected, actual, "smtp value of setting '#{setting}' do not match (#{expected} != #{actual})")
  end

  # 可选:若需确保邮件能正常发送,保留发送操作
  message.deliver_now
end

方案2:用Mock验证SMTP配置被正确应用

如果要验证配置确实被应用到发送流程,可借助Minitest的Mock功能拦截SMTP初始化:

test 'that the settings are according to a mailer' do
  expected_settings = email_provider.settings.with_indifferent_access
  # 预期的完整配置(含默认值)
  expected_full_settings = message.delivery_method.settings.with_indifferent_access.merge(expected_settings)

  # Mock SMTP投递方式,验证初始化时使用了正确配置
  smtp_mock = Minitest::Mock.new
  smtp_mock.expect(:deliver!, nil, [instance_of(Mail::Message)])
  smtp_mock.expect(:settings=, nil, [expected_full_settings])

  # 替换邮件对象的投递方式为mock
  message = @mailer.account_activation_confirm
  message.delivery_method(smtp_mock)

  message.deliver_now

  # 验证mock的预期调用是否执行
  smtp_mock.verify
end

内容的提问来源于stack exchange,提问作者Julien Lamarche

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 07:15:03