こちらの記事は、くふうカンパニー 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::RecordInvalid
とActiveRecord::RecordNotSaved
のケースは動作を指定してあげたいのです。
ここでいうと、このUserForm
classのsaveメソッドの返り値をfalseにしてあげて、errorsにも追加してあげる、と。
じゃあその他の例外についてはどうするとよさそうか?
ネットワークのエラーなどはこのformで検知する必要はなく、Railsのアプリ側でエラーは吐いてくれるためここでrescueする必要はありません。
さらに言えば、その他の例外を全部補足しようとすると rescueだけではできません。
なぜなら、このrescueだけの記述で処理に対応するのはStandardError
のサブクラスだけであるためです。
エラーにはこんなに種類があるのですね。
なるほど、このフォームでは想定し得るエラーは例外処理として捕捉するとして、その他のもっと広いエラーはいうなれば他の箇所でも起こりうるため、
より広い範囲をカバーするRails全体のエラーとしてしてあげればいいんだな、と。
ということで、このformのコードで捕捉するのは、ActiveRecord::RecordInvalid
とActiveRecord::RecordNotSaved
だけでよさそうです。
じゃあテストするぞ
その他のエラーは捕捉しないとわかったところで、テストも修正していきます。
context "raise other exception" do before { allow(station_save).to receive(:save!).and_raise(NoMethodError) } it { is_expected.to be_falsey } end
もともとActiveRecord::RecordInvalid
とActiveRecord::RecordNotSaved
以外の例外が発生した時に falseを返すようにしているんだから以上のようなテストが必要だろうと思って追加していました。
が、こちらはもう必要ない、と。
他の例外が起こった時はエラーとなってもOKということです。このformでテストする内容ではないですね、するとしたらRailsのもっと広いスコープ部分でテストするのかな?
さて、ここでformのクラスのsaveメソッドで何をテストするべきかを整理してみましょう。
saveメソッドで起こりうる事象は全部で4つです。
- valid?で invalidなパラメータがあってnilが返る
- 正常に保存される
- save!で例外が発生して falseが返る
ActiveRecord::RecordInvalid
- save!で例外が発生して falseが返る
ActiveRecord::RecordNotSaved
こうやって書き出してみると何をテストすればいいか、シンプルですよね。
この4つのふるまいをテストすればいいんです。
こう考えると、他の例外が起こりうるケースをテストしなくてもいいのかな?と考えた時にどうすればいいかわかりやすいと思います。
テストコードは省略します。このコードでいうところのtipsがあるとすれば、mockをうまく使うことでテストはシンプルに作ることができると思います!
まとめ
例外ってなんだかざっくりしかわかっていないなーと思っていたところが先輩の教えてくれたことがきっかけで理解が深まりました。
なんだかよくわからないと思っている部分は自分でググることも大切ですが、理解している人に聞く、教えてもらうのも大切ですね。
例外もしっかり理解して、堅牢に作っていくことができそうです!!