はじめに
こんにちは! イタンジ株式会社で更新退去くんというプロダクトを開発している沈です。
新卒一年目で学ぶことが多く、フロントエンド(Next.js)とバックエンド(Rails)両方の開発に携わり、システムがどのように動作するか、総合的に理解しています。直近は、Amazon SES(aws-sdk-ruby)が返すエラーに対して、エラーハンドリングを行いました。今回は、このエラーハンドリングを行った際のことを、ブログで発信していきたいと思います。
背景
メールの送信に失敗した場合に、エンドユーザーに原因が記載されたエラーメッセージを表示するため
仕様
SESから返されるエラーをApplicationMailerで捕捉し、それをユーザーフレンドリーなエラーメッセージに変換して、raiseし直します(raiseし直したエラーは、後にcatchして、APIレスポンスとして返します)。そしてフロントエンドで受け取り、画面に表示する。
課題
ApplicationMailerで各Mailerのエラーハンドリングを共通化し、エラーハンドリングの結果をRSpecでテストしたい
ActionMailer::MessageDeliveryのコードの定義
class HogeController < ApplicationController def fuga ... Notifier.welcome(User.first).deliver_now end end
module ActionMailer class MessageDelivery < Delegator ... # Method calls are delegated to the Mail::Message that's ready to deliver. def __getobj__ # :nodoc: @mail_message ||= processed_mailer.message end # Returns the resulting Mail::Message def message __getobj__ end ... # Delivers an email: # # Notifier.welcome(User.first).deliver_now # def deliver_now processed_mailer.handle_exceptions do processed_mailer.run_callbacks(:deliver) do message.deliver end end end end end
コントローラーでメールを送るコードは、 Notifier.welcome(User.first).deliver_now
です。welcome(User.first)
メソッドはActionMailer::MessageDeliveryオブジェクトを1つ返して、コードは最終的にmessage.deliver
に辿り着きます。
# The +ActionMailer::MessageDelivery+ class is used by # ActionMailer::Base when creating a new mailer. # <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy # created +Mail::Message+. You can get direct access to the # +Mail::Message+, deliver the email or schedule the email to be sent # through Active Job.
コメントの通り、Delegatorを継承することで、MessageDeliveryはMail::Messageのメソッドをそのまま呼び出せるようになります。具体的にいうと、__getobj__
メソッドは processed_mailer.message
を呼び出して Mail::Message を取得し、それを @mail_message
インスタンス変数にキャッシュします。ActionMailer::MessageDeliveryオブジェクトは、Mail::Messageをラップしていて、message
のclassはMail::Messageです。
RSpecでAmazon SESのエラーをMockを使用してテスト
ApplicationMailerのコード
class ApplicationMailer < ActionMailer::Base rescue_from Aws::SES::Errors::InvalidParameterValue do |e| ... raise MailerError.new( message: ... ) end end
rescue_fromは、特定の種類または複数の種類の例外を1つのコントローラ全体およびそのサブクラスで扱える手法です。
MailerのエラーハンドリングはApplicationMailerで行い、rescue_from
を使いAws::SES::Errors::InvalidParameterValue
が発生した場合それを補足して、次にMailerError
を返します(この時に、エラーメッセージをユーザーフレンドリーなものに変換します)。ここで、Aws::SES:Errors::InvalidParameterValue
が起きた際の動作をRSpecでテストするために、Mockを使用します。
allow_any_instance_of
Mockを使用するには、いくつかの手法があります。その中でinstance_double
メソッドは対象クラスのインスタンスをモックするために使用されます。一方、allow_any_instance_of
メソッドを使うと、対象クラスの全インスタンスに対して目的のメソッドをモック化できます。今回は、Mockしたいものが外部ライブラリ(Mail::Message
)のインスタンスであり、instance_double
を使ってモックを用意するのが難しいため、RSpec公式が非推奨と述べてますが、allow_any_instance_of
を使ってモックします。
def mock_raise_deliver_now_ses_error request_context = instance_double(Seahorse::Client::RequestContext) allow_any_instance_of(Mail::Message).to receive(:deliver).and_raise( Aws::SES::Errors::InvalidParameterValue.new(request_context, 'error message') ) end
deliver_now
の中身のメソッドをモックしたいので、Mail::Messageクラスの全インスタンス(message)に対して、deliverメソッドが呼ばれたときにAws::SES::Errors::InvalidParameterValue
を返すようにします。妥当な引数を渡したいので、rails c
でshow-source ${クラス名}
(show-source Aws::SES::Errors::InvalidParameterValue
)をすると、定義箇所のコードを見ることができます
今回であれば 以下のようになっていました。
# @param [Seahorse::Client::RequestContext] context # @param [String] message # @param [Aws::Structure] data def initialize(context, message, data = Aws::EmptyStructure.new) @code = self.class.code @context = context @data = data @message = message && !message.empty? ? message : self.class super(@message) end
まとめ
今回はallow_any_instance_ofメソッドを使って、ApplicationMailerで共通化した例外処理のテストを行いました。 振り返るとはっきりしたが、実際の仕事には横からタスクやミーティングが入る状態で、膨大なシステムの動作とコードを確認することは、まるで巨大迷宮の中で試行錯誤しながら出口を探している感じです。リーダーと先輩のおかげで第一弾としてのエラー処理を遂行することができました。大きく進歩する実感があります。