はじめに
RSpecでバリデーションのテストを書くとき、下記のように書くことが多いと思います。
it "is invalid without a name" do
user = User.new(name: nil)
user.valid?
expect(user.errors[:name]).to include("can't be blank")
end
かのEverydayRailsにも同様のサンプルが掲載されています。
しかし、I18nを導入してエラーメッセージなどを日本語化したときにこのテストは通過しなくなってしまいます。
そこで、次の様に修正します。
it "is invalid without a name" do
user = User.new(name: nil)
user.valid?
expect(user.errors[:name]).to include("を入力してください")
end
これでテストを通過することが出来ました。
しかしこれでは言語設定を変更するたびにテストが壊れて修正が必要になってしまうという問題が残ります。
解決方法
I18nの導入
ここでは記述しません。
[初学者]Railsのi18nによる日本語化対応などが参考になると思います。
I18n.translate メソッドを使用する
Railsガイド-基本的な参照、スコープ、ネストしたキーの項によると、次のようにしてエラーメッセージを呼び出すことが出来ます。
I18n.translate "activerecord.errors.messages.record_invalid"
アプリケーションのlocaleを日本に指定している状態で、rails c
で実行すると
>> I18n.translate "activerecord.errors.messages.record_invalid"
=> "バリデーションに失敗しました: %{errors}"
この様になります。
冒頭のバリデーションテストをこの方法で書き換えると次のようになります。
it "is invalid without a name" do
user = User.new(name: nil)
user.valid?
expect(user.errors[:name]).to include(I18n.t('errors.messages.blank'))
end
上記ではI18n.translate
のエイリアスであるI18n.t
を代わりに使用しています。
これで、言語設定が変わっても壊れないバリデーションテストを書くことができました。
エラーメッセージの呼び出し方色々
先程も参照したRailsガイド-基本的な参照、スコープ、ネストしたキーの項によると、下記の呼び出しはすべて等価ですので、好みの方法を使って書いてみてください。
I18n.t 'activerecord.errors.messages.record_invalid'
I18n.t 'errors.messages.record_invalid', scope: :activerecord
I18n.t :record_invalid, scope: 'activerecord.errors.messages'
I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
他のエラーメッセージを呼び出すには
Railsガイド-エラーメッセージ内での式展開の項に表でまとめられています。
I18n.translate "activerecord.errors.messages.record_invalid"
のrecord_invalid
の部分を表のメッセージ列の値に置き換えることで他のエラーメッセージを呼び出すことが出来ます
引数付きのエラーメッセージ呼び出し
rails c
でI18n.t('errors.messages.too_short')
を実行すると
>> I18n.t('errors.messages.too_short')
=> "は%{count}文字以上で入力してください"
このような式展開が使われていることから引数にcount: [最低文字数]
を付加すれば良いと分かります。
>> I18n.t('errors.messages.too_short', count: 6)
=> "は6文字以上で入力してください"
以上からパスワード作成時などに6文字以上のバリデーションを設定しているときは、次のようにテストすることが出来ます。
it "is invalid with a shoot password" do
user = User.new(password: 'a' * 5)
user.valid?
expect(user.errors[:password]).to include(I18n.t('errors.messages.too_short', count: 6))
end
Railsガイド-エラーメッセージ内での式展開の項の表の式展開列に使用されている式展開が書かれているのでご参照ください。
他の方法(2020/1/24追記)
本稿のコメントで@jnchitoさんより、自身が以前書いたエントリで近い内容を紹介していたことを教えて頂きました。
どのフィールドにどの検証エラーが追加されたのかを、「表示言語やエラーメッセージに依存しない形で」テストする方法
このエントリで紹介した方法との違い
@jnchitoさんの方法ではi18nを介さず、エラーの種類自体をチェックする方式となっています。一方で私が紹介したのは、i18nで翻訳後のエラーメッセージをチェックする方式となっています。
この2つの方式の違いが出るのは、例えばja.ymlの中に翻訳後のエラーメッセージが登録されていなかったときです。
errors:
format: "%{attribute}%{message}"
messages:
accepted: を受諾してください
# ja.errors.messages.blankをコメントアウト
#blank: を入力してください
confirmation: と%{attribute}の入力が一致しません
この状態でテストを実行すると@jnchitoさんの方法ではテストを通過します。
一方、私が紹介した方法では下記のようにエラーが発生します。
1) User is invalid without name
Failure/Error: expect(user.errors[:name]).to include(I18n.t('errors.messages.blank'))
expected ["translation missing: ja.activerecord.errors.models.user.attributes.email.blank"] to include "translation missing: ja.errors.messages.blank"
# ./spec/models/user_spec.rb:18:in `block (3 levels) in <top (required)>'
# -e:1:in `<main>'
個人的には@jnchitoさんの方法では
- エラーの種類のチェックとviewに表示されている翻訳後のメッセージのチェックを分離できる
- より短くシンプルなテストを書ける
などのメリットがあると思います。
一方でこの記事で紹介した方法では
- 翻訳後のエラーメッセージが
ja.yml
等に正しく登録されていることをチェックできる - 登録されているエラーメッセージがviewに正しく表示されていることをチェックできる
という点でメリットがあると思います。(他にもまだメリット、デメリットはあるかもしれません。)
細かい違いですが、その時やりたいことや好みに応じて使い分けると良いかと思います。
おわりに
I18nで日本語化した時に、バリデーションテストでエラーメッセージをベタ書きしてチェックする方法以外のやり方を調べましたが、ピンポイントで書かれている記事が見つからなかったため書いてみました。
同じ疑問を持った誰かのお役に立てればと思います。
※他の方法(2020/1/24追記)で紹介した通り、近い内容のエントリが既にありました。ご指摘して頂いた@jnchitoさんありがとうございます。