はじめに
Railsアプリケーションでモデルのバリデーションを検証するをテストケースを想定します。
たとえば、「姓が空文字列であれば検証エラーが発生すること」をテストしようとすると、次のようなテストが書けます。
john = User.new(first_name: '', last_name: 'Lennon')
john.valid?
expect(john.errors[:first_name]).to include("can't be blank")
ですが、"can't be blank"はあくまで英語表示だった場合のエラーメッセージです。
もし日本語表示だったら次のようになります。
expect(john.errors[:first_name]).to include("を入力してください")
上記の方法で困ること
この方法でも十分といえば十分なのですが、表示言語を切り替えられるタイプのRailsアプリケーション(国際化されたRailsアプリケーション)だと、テストが何か特定の言語(英語や日本語)に依存するのは少し気持ち悪い気がします。
それに、何らかの理由でエラーメッセージを変更すると、テストをいちいち修正しなければならない、という問題も発生します。
解決策
上のようなテストを表示言語やエラーメッセージに依存しない形で(つまり、エラーメッセージのキー情報だけを参照する形で)テストするには次のように書きます。
expect(john.errors.added?(:first_name, :blank)).to be_truthy
# または(predicateマッチャを使う場合)
expect(john.errors).to be_added(:first_name, :blank)
後者の方が短く書けますが、個人的には前者の書き方の方が明示的でわかりやすい気がします。
:blank って何?どこからやってきたの??
added?
メソッドの第2引数として渡している:blank
は必須エラーが発生したときに使用されるエラーメッセージのキー情報です。
en:
errors:
messages:
blank: "can't be blank"
詳しくは以下のRailsガイドを参照してください。
参考:Rails 国際化 (i18n) API - Rails ガイド
応用:メッセージに動的な値が埋め込まれる場合
検証エラーの中には、メッセージ内に動的に値を埋め込むものがあります。以下はその具体例です。
en:
errors:
messages:
too_short:
one: "is too short (minimum is 1 character)"
# %{count}には動的な値が埋め込まれる
other: "is too short (minimum is %{count} characters)"
class User < ApplicationRecord
# 姓は2文字以上あることが必須
validates :first_name, length: { minimum: 2 }
end
# 姓をわざと1文字にする
john = User.new(first_name: 'J', last_name: 'Lennon')
john.valid?
# エラーメッセージの%{count}の部分に"2"が埋め込まれる
john.errors[:first_name]
#=> ["is too short (minimum is 2 characters)"]
こういうケースのテストでは次のように、:too_short
だけでなくcount
が2であることも指定する必要があります。
expect(john.errors.added?(:first_name, :too_short, count: 2)).to be_truthy
# または(predicateマッチャを使う場合)
expect(john.errors).to be_added(:first_name, :too_short, count: 2)
さらに:added?メソッドの第2引数と第3引数を調べる方法
added?
メソッドの第2引数と第3引数に何を指定すればいいかわからない場合は、次のようにerrors.details[(フィールド名)]
の中身を確認するとわかります。
john.errors.details[:first_name]
#=> [{:error=>:too_short, :count=>2}]
上の出力例では:too_short, :count=>2
とあるので、これがadded?
メソッドに渡す第2引数と第3引数になります。
追記:Rails 6では errors.of_kind? が便利!
Rails 6からは errors.of_kind?
が使えます。これを使うとadded?
メソッドで必要だった count: 2
を指定せずに、メッセージのキー情報だけで検証エラーの有無を確認できます。
expect(john.errors.of_kind?(:first_name, :too_short)).to be_truthy
# または(predicateマッチャを使う場合)
expect(john.errors).to be_of_kind(:first_name, :too_short)
(Objectクラスに元から用意されている)kind_of?
ではなくて、 of_kind?
なので注意してください。(僕は最初うっかり間違えました😣)
謝辞
コメント欄でこのメソッドを教えてくれた @ttakuru88 さん、どうもありがとうございました!
まとめ
というわけで、この記事では「表示言語やエラーメッセージに依存しない形で検証エラーの有無をテストする方法」を紹介しました。
必ずしもエラーメッセージを直接比較する方法が悪いわけではありません。
ですが、こうしたアプローチもあることを知っておくと役に立つ場面があるかもしれません😉