Edited at

どのフィールドにどの検証エラーが追加されたのかを、「表示言語やエラーメッセージに依存しない形で」テストする方法


はじめに

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.yml

en:

errors:
messages:
blank: "can't be blank"

詳しくは以下のRailsガイドを参照してください。

参考:Rails 国際化 (i18n) API - Rails ガイド


応用:メッセージに動的な値が埋め込まれる場合

検証エラーの中には、メッセージ内に動的に値を埋め込むものがあります。以下はその具体例です。


en.yml

en:

errors:
messages:
too_short:
one: "is too short (minimum is 1 character)"
# %{count}には動的な値が埋め込まれる
other: "is too short (minimum is %{count} characters)"


user.rb

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 さん、どうもありがとうございました!


まとめ

というわけで、この記事では「表示言語やエラーメッセージに依存しない形で検証エラーの有無をテストする方法」を紹介しました。

必ずしもエラーメッセージを直接比較する方法が悪いわけではありません。

ですが、こうしたアプローチもあることを知っておくと役に立つ場面があるかもしれません😉