Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

まとめ

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

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

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away