
Rails ActionMailer 变更日志精读_deliver 回调缺失动作检查与 assert_part / assert_no_part 邮件断言【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/rails本文基于当前仓库中actionmailer/CHANGELOG.md的两条核心变更记录展开一是config.action_mailer.raise_on_missing_callback_actions配置项对only:/except:条件下_deliver回调的缺失动作检查二是ActionMailer::TestCase新增的assert_part/assert_no_part断言。读完本文你将理解这两项能力背后的源码实现AbstractController::Callbacks与ActionMailer::TestCase并能在自己项目中正确配置、使用并验证它们。一、变更日志的原始内容actionmailer/CHANGELOG.md 当前记录了两项变更Add support forconfig.action_mailer.raise_on_missing_callback_actionswhen using_delivercallbacks withonly:andexcept:options.Iaroslav 贡献Addassert_partandassert_no_parttoActionMailer::TestCaseSean Doyle 贡献并附带如下用法示例test assert MyMailer.welcome HTML and text parts do mail MyMailer.welcome(Hello, world) assert_part :text, mail do |text| assert_includes text, Hello, world end assert_part :html, mail do |html| assert_dom html.root, p, Hello, world end end下面分别结合仓库源码把这两项变更的实现机制与实战用法讲透。二、raise_on_missing_callback_actions让写错回调动作名尽早暴露2.1 这个配置解决什么问题Rails 的回调系统before_action、after_action、around_action以及 ActionMailer 的before_deliver、after_deliver、around_deliver都支持only:和except:选项来限定回调作用的 action。问题在于如果你在选项里写了一个不存在的 action 名拼写错误、action 被重命名后忘记同步Rails 默认不会报任何错只是回调静默地不匹配这类 bug 往往很难被察觉。raise_on_missing_callback_actions正是为此而设的开关设为true时如果回调的only:/except:列表中包含一个控制器或 Mailer上不存在的 action框架会抛出AbstractController::ActionNotFound异常并在错误信息中指明是哪个 action 找不到、属于哪个回调默认值为false即保持向后兼容的宽松行为。当前仓库的官方配置文档 guides/source/configuring.md 中对该项的描述为Mirrorsconfig.action_controller.raise_on_missing_callback_actions, but applies to mailers. Defaults tofalse.也就是说config.action_mailer.raise_on_missing_callback_actions是对config.action_controller.raise_on_missing_callback_actions的镜像配置专门作用于 Mailer。2.2 源码级实现该配置项在底层是一个class_attribute定义在 actionpack/lib/abstract_controller/callbacks.rbclass_attribute :raise_on_missing_callback_actions, instance_predicate: false, default: false真正的检查发生在ActionFilter#match?中actionpack/lib/abstract_controller/callbacks.rbdef match?(controller) if controller.raise_on_missing_callback_actions missing_action actions.find { |action| !controller.available_action?(action) } if missing_action # ... 构造错误信息 ... raise ActionNotFound.new(message, controller, missing_action) end end actions.include?(controller.action_name) end关键流程only:/except:选项在回调注册时经过_normalize_callback_options归一化被包装成ActionFilter实例其内部保存了目标 action 名集合每次回调执行前调用match?判断是否命中当前 action若开关开启match?会先检查ActionFilter列出的 action 是否都真实存在发现缺失即抛出ActionNotFound错误信息形如The non_existent_action action could not be found for the [...] callback on CallbackMailer, but it is listed in the controllers :except option.2.3 ActionMailer 侧如何接上这套机制ActionMailer 自己的deliver回调族定义在 actionmailer/lib/action_mailer/callbacks.rbmodule ActionMailer module Callbacks extend ActiveSupport::Concern DEFAULT_INTERNAL_METHODS [:_run_deliver_callbacks].freeze # :nodoc: included do include ActiveSupport::Callbacks define_callbacks :deliver, skip_after_callbacks_if_terminated: true end module ClassMethods def before_deliver(*filters, blk) _insert_callbacks(filters, blk) do |name, options| set_callback(:deliver, :before, name, options) end end # after_deliver / around_deliver 同构 end end end_insert_callbacks与ActionFilter归一化逻辑继承自AbstractController::Callbacks因此before_deliver(only: :xxx)等写法天然复用了上面同一套raise_on_missing_callback_actions检查机制——这正是本条 CHANGELOG 变更的意义让 Mailer 的_deliver回调也支持这个检查开关。仓库中的测试用例完整验证了这一行为见 actionmailer/test/callbacks_test.rbtest deliver callbacks with except option ignore missing actions by default do CallbackMailer.test_raise_action.deliver_now assert_includes CallbackMailer.deliver_callback_log, [:except_action, test_raise_action] end test raise_on_missing_callback_actions raises for missing deliver callback actions do CallbackMailer.raise_on_missing_callback_actions true error assert_raises(AbstractController::ActionNotFound) do CallbackMailer.test_raise_action.deliver_now end assert_match(/The non_existent_action action could not be found/, error.message) end对应的测试 Mailer 在 actionmailer/test/mailers/callback_mailer.rb其中注册了only:与except:两种条件的before_deliver/after_deliver/around_deliver回调例如before_deliver(only: :test_message) do CallbackMailer.deliver_callback_log :before_only end before_deliver(except: [:test_message, :non_existent_action]) do CallbackMailer.deliver_callback_log [:except_action, action_name] endexcept:列表中包含了一个不存在的:non_existent_action——默认情况下被静默忽略开启开关后立即抛错。2.4 项目中的配置实践如果你用 Rails 生成新应用development与test环境模板默认启用了控制器侧的对应开关见 railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt 与 railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.ttconfig.action_controller.raise_on_missing_callback_actions true对 Mailer 侧你可以在环境配置中按相同方式添加config.action_mailer.raise_on_missing_callback_actions true建议在开发与测试环境优先开启把拼写错误扼杀在 CI 阶段。三、assert_part/assert_no_part对邮件部件的结构化断言3.1 为什么需要它们多部件multipart邮件同时包含text/plain与text/html两个 body 部件还可能嵌套附件。旧做法是手动mail.html_part.decoded、mail.text_part.decoded再断言遇到非 multipart 邮件或缺少某部件时会拿到nil断言逻辑散落且容易写错。assert_part/assert_no_part把找到匹配 MIME 类型的部件并解码这件事封装成了框架级断言。3.2 API 与实现实现位于 actionmailer/lib/action_mailer/test_case.rb# Assert that a Mail instance has a part matching the content type. # If the Mail is multipart, extract and decode the appropriate part. Yield the decoded part to the block. # # By default, assert against the last delivered Mail. def assert_part(content_type, mail last_delivered_mail!) mime_type Mime[content_type] part [*mail.all_parts, mail].find { |part| mime_type.match?(part.mime_type) } decoder _decoders[mime_type] assert_not_nil part, expected part matching #{mime_type} in #{mail.inspect} yield decoder.call(part.decoded) if block_given? end # Assert that a Mail instance does not have a part with a matching MIME type def assert_no_part(content_type, mail last_delivered_mail!) mime_type Mime[content_type] part [*mail.all_parts, mail].find { |part| mime_type.match?(part.mime_type) } assert_nil part, expected no part matching #{mime_type} in #{mail.inspect} end要点解读第一个参数是 MIME 符号:text、:html等内部经Mime[...]转为 MIME 类型后用match?做匹配因此:html能匹配text/html部件第二个参数可选缺省断言对象是last_delivered_mail!最后一次deliver_now产生的邮件显式传入某个Mail实例则断言该实例部件查找覆盖扁平与嵌套[*mail.all_parts, mail]既展开所有嵌套部件也把邮件本体纳入查找范围所以单部件非 multipart邮件同样适用解码器由 MIME 类型决定_decoders在 actionmailer/lib/action_mailer/test_case.rb 中定义Mime[:html]的解码器会把 HTML 解析为Rails::Dom::Testing.html_document其余类型默认原样返回——这就是 CHANGELOG 示例中assert_dom html.root, p, Hello, world能工作的原因block 参数不是字符串而是可执行 DOM 断言的文档对象。3.3 典型用法CHANGELOG 中的原始示例断言显式传入的邮件实例test assert MyMailer.welcome HTML and text parts do mail MyMailer.welcome(Hello, world) assert_part :text, mail do |text| assert_includes text, Hello, world end assert_part :html, mail do |html| assert_dom html.root, p, Hello, world end end结合源码文档actionmailer/lib/action_mailer/test_case.rb给出的缺省用法断言最后投递的邮件UsersMailer.create(user).deliver_now assert_part :text do |text| assert_includes text, Welcome, #{user.email} end assert_part :html do |html| assert_dom html.root, h1, text: Welcome, #{user.email} end反向断言assert_no_part用来验证某邮件只应为纯文本之类的约束UsersMailer.create(user).deliver_now assert_no_part :html assert_no_part :text3.4 测试用例验证这些断言的行为由 actionmailer/test/assert_select_email_test.rb 完整覆盖包括对最后一次投递邮件的断言test_assert_part_last_mail_delivery与对显式mail参数的断言test_assert_part_with_mail_argument不传 block 时仅校验部件存在test_assert_part_without_blockassert_no_part的正反两个方向test_assert_no_part嵌套在附件之下的 body 部件也能被找到test_assert_part_finds_body_parts_nested_under_attachment、test_assert_no_part_detects_body_parts_nested_under_attachment——这印证了实现中mail.all_parts递归展开部件树的行为。四、小结本仓库 actionmailer/CHANGELOG.md 记录的两项变更一个面向开发期防御、一个面向测试期验证config.action_mailer.raise_on_missing_callback_actions让before_deliver/after_deliver/around_deliver回调中的only:/except:拼写错误在运行期立即以AbstractController::ActionNotFound暴露出来其底层检查机制与ActionController同名配置完全同源定义于 actionpack/lib/abstract_controller/callbacks.rbassert_part/assert_no_part为ActionMailer::TestCase提供了按 MIME 类型定位并解码邮件部件的断言HTML 部件会被解析为可执行 DOM 断言的文档对象实现位于 actionmailer/lib/action_mailer/test_case.rb。两项能力均已在仓库测试套件中有对应的验证用例actionmailer/test/callbacks_test.rb、actionmailer/test/assert_select_email_test.rb可直接作为行为依据参考。【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/rails创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考