121
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-23

はじめに

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

まとめ

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

必ずしもエラーメッセージを直接比較する方法が悪いわけではありません。
ですが、こうしたアプローチもあることを知っておくと役に立つ場面があるかもしれません😉

121
73
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
121
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?