【rails】rspecで例外処理をテストする方法

例外処理とは

例外処理は通常のフローでエラーが発生した時にどうするか、ということ。
通常のsaveとかでは以下のようにif節でsaveできない時を指定できる。

def index
  @orders = Order.all
end

def new
  @order = Order.new
end

def create
  if @order.save
    redirect_to order_index_path
  else
    flash[:error] = "正しく入力してください"
    redirect_to order_new_path
  end
end

ところが、Mailerとかだと基本的に遅れることが前提になっているためエラーの検知が難しい。
そこで、例外処理を入れておく。

例外処理の書き方

例えばOrderModelでorderをcreateできたらthanksメールを送るとする。
その際のコードはこんな感じ。

class OrdersController < ApplicationController
  def index
    @orders = Order.all
  end

  def new
    @order = Order.new
  end

  def create
    if @order.save
      ::OrderMailer.thanks(@order).deliver
      redirect_to order_index_path
    else
      flash[:error] = "正しく入力してください"
      redirect_to order_new_path
    end
  end
end

class OrderMailer < ApplicationMailer
  def thanks(order)
    ...
    # ここはメールのオプションとかを記載(今回は省略)
  end
end

ここで、メールを送る動作の時にエラーが起こると本来indexにリダイレクトするところがエラー描写になってしまう。
そこで、ここに例外処理を追加しておく。

  def create
    if @order.save
      begin
        ::OrderMailer.thanks(@order).deliver
      rescue => e  # eはエラーの内容を表示。
        flash[:error] = "メールの送信に失敗しました。登録は完了しています。"
      end
      redirect_to order_index_path
    else
      flash[:error] = "正しく入力してください"
      redirect_to order_new_path
    end
  end

上ではメールが送れなかった時にindexページで送信失敗のフラッシュを表示させている。
rescue => eではエラーの内容をrescue内で使えるようにしている。Bugsnagとかでエラー検知している場合はそれを送ることも可能。

例外を起こす

通常ローカル環境でメール送信のエラーを起こすことはできないので、確認のためには強制的に発生させる必要がある。
そこで使うのがraiseコマンド。

  def create
    if @order.save
      begin
        raise
        ::OrderMailer.thanks(@order).deliver
      rescue => e  # eはエラーの内容を表示。
        flash[:error] = "メールの送信に失敗しました。登録は完了しています。"
      end
      redirect_to order_index_path
    else
      flash[:error] = "正しく入力してください"
      redirect_to order_new_path
    end
  end

これでbegin節内で擬似的にエラーを起こすことができる。

rspecで例外処理を起こしたい

さて本題。ローカル環境でエラーを起こせないということはrspecでも通常はメール送信エラーは起こらない。
そこで、強制的にエラーを発生させる必要がある。

RSpec.describe OrdersController, type: :controller do
  context 'createページに関して' do
    let(:order) = { FactoryBot.attributes_for(:order) }

    describe 'thanksメールの送信成功' do
      before do
        order_mailer = double("order_mailer")
        expect(OrderMailer).to receive(:thanks).with(instance_of(Order)).once.and_return(order_mailer)
        expect(order_mailer).to receive(:deliver).once
      end

      post: create
      expect(response).to redirect_to(order_index_path)
    end
    
    # 例外処理
    describe 'thanksメールの送信失敗' do
      before do
        order_mailer = double("order_mailer")
        allow(OrderMailer).to receive(:thanks).and_raise(RuntimeError)
      end

      post: create
      expect(response).to redirect_to(order_index_path)
    end
  end
end

通常はモックを作ってexpectでreceiveさせるところをallow.toでand_raiseで強制的にraiseコマンドを発生させている。
Mailerで例外処理が発生した場合e.class = RuntimeErrorとなるので、今回はそれを記載しておいた。

まとめ

今回はメールで例外処理を起こしたが、URLのパラメータなんかはユーザー操作で変えることができてしまうので想定していない挙動を起こすこともあり得る。
そんな時は例外処理で対応を記載しておくとユーザーにエラーページを見せずにページ遷移ができる。