【RSpec】言語非依存の壊れにくいバリデーションテストを作る

# はじめに
RSpecでバリデーションのテストを書くとき、下記のように書くことが多いと思います。

```spec/models/user_spec.rb
  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](https://leanpub.com/everydayrailsrspec-jp/read#leanpub-auto-section-18)にも同様のサンプルが掲載されています。

しかし、I18nを導入してエラーメッセージなどを日本語化したときにこのテストは通過しなくなってしまいます。
そこで、次の様に修正します。

```spec/models/user_spec.rb
  it "is invalid without a name" do
    user = User.new(name: nil)
    user.valid?
    expect(user.errors[:name]).to include("を入力してください")
  end
```

これでテストを通過することが出来ました。

しかしこれでは**言語設定を変更するたびにテストが壊れて修正が必要になってしまう**という問題が残ります。

# 解決方法
### I18nの導入
ここでは記述しません。
[[初学者]Railsのi18nによる日本語化対応](https://qiita.com/shimadama/items/7e5c3d75c9a9f51abdd5)などが参考になると思います。
### I18n.translate メソッドを使用する
[Railsガイド-基本的な参照、スコープ、ネストしたキー](https://railsguides.jp/i18n.html#%E8%A8%B3%E6%96%87%E3%81%AE%E5%8F%82%E7%85%A7)の項によると、次のようにしてエラーメッセージを呼び出すことが出来ます。
> I18n.translate "activerecord.errors.messages.record_invalid"

アプリケーションのlocaleを日本に指定している状態で、`rails c`で実行すると

```
>> I18n.translate "activerecord.errors.messages.record_invalid"
=> "バリデーションに失敗しました: %{errors}"
```

この様になります。
冒頭のバリデーションテストをこの方法で書き換えると次のようになります。

```spec/models/user_spec.rb
  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ガイド-基本的な参照、スコープ、ネストしたキー](https://railsguides.jp/i18n.html#%E8%A8%B3%E6%96%87%E3%81%AE%E5%8F%82%E7%85%A7)の項によると、下記の呼び出しはすべて等価ですので、好みの方法を使って書いてみてください。

> 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ガイド-エラーメッセージ内での式展開](https://railsguides.jp/i18n.html#%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E5%86%85%E3%81%A7%E3%81%AE%E5%BC%8F%E5%B1%95%E9%96%8B)の項に表でまとめられています。

`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文字以上のバリデーションを設定しているときは、次のようにテストすることが出来ます。

```spec/models/user_spec.rb
  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ガイド-エラーメッセージ内での式展開](https://railsguides.jp/i18n.html#%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E5%86%85%E3%81%A7%E3%81%AE%E5%BC%8F%E5%B1%95%E9%96%8B)の項の表の式展開列に使用されている式展開が書かれているのでご参照ください。

# 他の方法(2020/1/24追記)
本稿のコメントで[@jnchito](https://qiita.com/jnchito)さんより、自身が以前書いたエントリで近い内容を紹介していたことを教えて頂きました。

[どのフィールドにどの検証エラーが追加されたのかを、「表示言語やエラーメッセージに依存しない形で」テストする方法](https://qiita.com/jnchito/items/6cf94a82e719c1dca5f1)

### このエントリで紹介した方法との違い
@jnchitoさんの方法ではi18nを介さず、**エラーの種類自体をチェックする方式**となっています。一方で私が紹介したのは、i18nで**翻訳後のエラーメッセージをチェックする方式**となっています。

この２つの方式の違いが出るのは、例えばja.ymlの中に翻訳後のエラーメッセージが登録されていなかったときです。

```config/locale/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](https://qiita.com/jnchito)さんの方法では

- エラーの種類のチェックとviewに表示されている翻訳後のメッセージのチェックを分離できる
- より短くシンプルなテストを書ける

などのメリットがあると思います。

一方でこの記事で紹介した方法では

- 翻訳後のエラーメッセージが`ja.yml`等に正しく登録されていることをチェックできる
- 登録されているエラーメッセージがviewに正しく表示されていることをチェックできる

という点でメリットがあると思います。（他にもまだメリット、デメリットはあるかもしれません。）

細かい違いですが、その時やりたいことや好みに応じて使い分けると良いかと思います。

# おわりに
I18nで日本語化した時に、バリデーションテストでエラーメッセージをベタ書きしてチェックする方法以外のやり方を調べましたが、ピンポイントで書かれている記事が見つからなかったため書いてみました。
同じ疑問を持った誰かのお役に立てればと思います。

※[他の方法(2020/1/24追記)](#他の方法(※2020/1/24追記))で紹介した通り、[近い内容のエントリ](https://qiita.com/jnchito/items/6cf94a82e719c1dca5f1)が既にありました。ご指摘して頂いた@jnchitoさんありがとうございます。

# 参考
- [[初学者]Railsのi18nによる日本語化対応](https://qiita.com/shimadama/items/7e5c3d75c9a9f51abdd5)
- [EverydayRails](https://leanpub.com/everydayrailsrspec-jp/read#leanpub-auto-section-18)
- [Railsガイド-基本的な参照、スコープ、ネストしたキー](https://railsguides.jp/i18n.html#%E8%A8%B3%E6%96%87%E3%81%AE%E5%8F%82%E7%85%A7)
- [Railsガイド-エラーメッセージ内での式展開](https://railsguides.jp/i18n.html#%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E5%86%85%E3%81%A7%E3%81%AE%E5%BC%8F%E5%B1%95%E9%96%8B)
- [どのフィールドにどの検証エラーが追加されたのかを、「表示言語やエラーメッセージに依存しない形で」テストする方法](https://qiita.com/jnchito/items/6cf94a82e719c1dca5f1)