SakuraWi - BLog

WEBエンジニア。聴いたお話をまとめておく倉庫的な。スタックストックスタック!

Railsにおける例外処理の考え方とテストでは何をテストするべきか?講座


こちらの記事は、くふうカンパニー Advent Calendar 2018の21日目の記事となります。

くふうカンパニー Advent Calendar 2018 - Qiita

例外処理、みなさんどうするべきなのかちゃんと把握しているでしょうか?

正直言います。 ワタクシ、ちゃんと理解していませんでした!!!

この度、会社の先輩に教えていただくことがありましたので、そちらをまとめます!

実はこちらの記事がサラサラ読めて最高にわかりやすいので、こちらも必読です。

Railsアプリケーションにおけるエラー処理(例外設計)の考え方 - Qiita

例外処理ってなんだろう?

簡単に言うならば、エラーの処理ってところでしょうか。

イメージとしては、Railsを書いていて開発中にアクセスして赤い画面がでてますよね。あれ、エラー画面です。

例外が発生するとアプリケーションを止めてどこでエラーが生じたのかを表示、発生させます。

エラーがでたらアプリケーションが止まっちゃっている状態のような感じですね。

で、止まっちゃうと困る、みたいなケースもありえるわけです。 そんな時に、こういう例外だったらこう動作してほしいというケースに対応したりもする必要がでてきます。

今回はcsvファイルをアップロードするフォームオブジェクトを作っていたときの例でみていきましょう。

実際にこんなフォームオブジェクトのケースがありました

実際に指摘していただいたときのコードを紹介しましょう。

class UserForm
    include ActiveModel::Model

    attr_writer :csv_table
    attr_accessor :csv

    validates :csv, presence: true
    validate :ensure_csv_data, if: :check_csv_presence

    def save
      return unless valid?

      begin
        UserRegister.new(csv_file_path).save!
      rescue ActiveRecord::RecordInvalid
        errors.add(:invalid_error, "が含まれています")
        return false
      rescue ActiveRecord::RecordNotSaved
        errors.add(:save_error, "保存に失敗しました")
        return false
      rescue
        errors.add(:save_error, "に失敗しました")
        return false
      end
    end

  # 一部省略
end

こんな感じで書いていて「これは不要なじゃないか?」と指摘があった部分は、

    rescue
        errors.add(:save_error, "に失敗しました")
        return false

この部分ですね。

これを書いたときの自分の思想としては

「なにか例外が起こったらひとまず、例外をキャッチしてerrorsに追加。falseを返しておこう。」

といった具合でした。

これ!ダメです。色々とまずいです。反省です。順を追って説明していきます。

ちょっと待とう!例外のそもそもの考え方。

そもそもエラーにはどういうエラーがあるのかを認識しましょう。

エラーには、業務エラーとシステムエラーがあります。

業務エラーというものは、フォームへの入力に全角が入ってしまっている、メールアドレスが正しいフォーマットではない、といったものです。

システムエラーには、ネットワークが途中で切れてしまった、データベースが落ちてしまっているといったことがあります。

で、業務エラーというものはユーザが入力を間違えてしまったりしているので、ユーザに修正してもらうことで正常な入力をしてもらうことができます。

Rails書いている人は valid?して、エラー内容をフォームに表示させることを1度はやったことがあるはずです。

システムエラーは、ユーザではなくって開発者側で復旧させる必要があります。

ユーザにはどうしようもないですからね。

ということで、ここらへんを頭に入れた状態で先ほどのコードをみながら修正していきましょう。

さらには、何をテストするのといいのかを考えていきましょう。

不要な例外処理を消す

先ほどのコードを再度掲載しますね。

class UserForm
    include ActiveModel::Model

    attr_writer :csv_table
    attr_accessor :csv

    validates :csv, presence: true
    validate :ensure_csv_data, if: :check_csv_presence

    def save
      return unless valid?

      begin
        UserRegister.new(csv_file_path).save!
      rescue ActiveRecord::RecordInvalid
        errors.add(:invalid_error, "が含まれています")
        return false
      rescue ActiveRecord::RecordNotSaved
        errors.add(:save_error, "保存に失敗しました")
        return false
      rescue
        errors.add(:save_error, "に失敗しました")
        return false
      end
    end

  # 一部省略
end

ふむふむ。業務エラーとシステムエラーな部分に着目してみてみましょう。

Railsでは業務エラーは ActiveModel::Model や自分でvalidationを追加することでほぼカバーできます。

システムエラーはこのコードでいうとUserRegister.save!の中で起こりうるようにしています。 ここでデータベースに保存する処理がかいてあるため、業務的にvalidationを突破して要件をみたした内容を保存しようとするときにDBの問題でエラーがおきたとします。

これは基本的には ActiveRecordで起こりうることが想定できるため、ActiveRecord::RecordInvalidActiveRecord::RecordNotSavedのケースは動作を指定してあげたいのです。

ここでいうと、このUserFormclassのsaveメソッドの返り値をfalseにしてあげて、errorsにも追加してあげる、と。

じゃあその他の例外についてはどうするとよさそうか?

ネットワークのエラーなどはこのformで検知する必要はなく、Railsのアプリ側でエラーは吐いてくれるためここでrescueする必要はありません。

さらに言えば、その他の例外を全部補足しようとすると rescueだけではできません。

なぜなら、このrescueだけの記述で処理に対応するのはStandardErrorのサブクラスだけであるためです。

エラーにはこんなに種類があるのですね。

Ruby Exceptions

なるほど、このフォームでは想定し得るエラーは例外処理として捕捉するとして、その他のもっと広いエラーはいうなれば他の箇所でも起こりうるため、

より広い範囲をカバーするRails全体のエラーとしてしてあげればいいんだな、と。

ということで、このformのコードで捕捉するのは、ActiveRecord::RecordInvalidActiveRecord::RecordNotSavedだけでよさそうです。

じゃあテストするぞ

その他のエラーは捕捉しないとわかったところで、テストも修正していきます。

context "raise other exception" do
  before { allow(station_save).to receive(:save!).and_raise(NoMethodError) }

  it { is_expected.to be_falsey }
end

もともとActiveRecord::RecordInvalidActiveRecord::RecordNotSaved以外の例外が発生した時に falseを返すようにしているんだから以上のようなテストが必要だろうと思って追加していました。

が、こちらはもう必要ない、と。

他の例外が起こった時はエラーとなってもOKということです。このformでテストする内容ではないですね、するとしたらRailsのもっと広いスコープ部分でテストするのかな?

さて、ここでformのクラスのsaveメソッドで何をテストするべきかを整理してみましょう。

saveメソッドで起こりうる事象は全部で4つです。

  • valid?で invalidなパラメータがあってnilが返る
  • 正常に保存される
  • save!で例外が発生して falseが返る ActiveRecord::RecordInvalid
  • save!で例外が発生して falseが返る ActiveRecord::RecordNotSaved

こうやって書き出してみると何をテストすればいいか、シンプルですよね。

この4つのふるまいをテストすればいいんです。

こう考えると、他の例外が起こりうるケースをテストしなくてもいいのかな?と考えた時にどうすればいいかわかりやすいと思います。

テストコードは省略します。このコードでいうところのtipsがあるとすれば、mockをうまく使うことでテストはシンプルに作ることができると思います!

まとめ

例外ってなんだかざっくりしかわかっていないなーと思っていたところが先輩の教えてくれたことがきっかけで理解が深まりました。

なんだかよくわからないと思っている部分は自分でググることも大切ですが、理解している人に聞く、教えてもらうのも大切ですね。

例外もしっかり理解して、堅牢に作っていくことができそうです!!