モデルの属性テスト: 小林研 Rails Tips (55)

このページの内容は以下のリポジトリに1日遅れで反映されます(記事執筆前に前日分をコミットしています)。

https://github.com/hkob/hkob_blog

はじめに

Rails Tips の 55 回目です。model に関してテストをします。

Rails をはじめよう - Railsガイド

7.3.3 バリデーションとエラーメッセージの表示

Rails ガイドではある程度動いてからバリデーションなどを追加していました。今回は、モデルの段階でちゃんとテストを実施しましょう。article_spec.rb の先頭部分を以下のように修正します。ここでは、最初に title と body が空の時にバリデーションに引っかかることを確認します。属性に関する共通テストのうち、存在制約と削除可能制約を残します。

require "rails_helper"

RSpec.describe Article, type: :model do
  let(:article) { articles :article1 }
  let(:can_delete) { articles :can_delete }

  context "属性に関する共通テスト" do
    subject { can_delete }

    it_behaves_like "存在制約", %i[title body]
    it_behaves_like "削除可能制約"
  end

  (以下のコメントは中略)
end

使っている shared example はここで説明していました。ここに書かれているそれぞれの shared example を spec/support/shared_exmaple.rb に記述しておきます。

ここで残っているのは articles の fixture data です。これらは、spec/fixtures の下に yaml ファイルとして用意します。

mkdir -p spec/fixtures
touch spec/fixtures/articles.yml

この状態で、yml を保存したところ、guard で以下のようなエラーが出てしまいました。singularize が見つかっていません。これは単数系・複数形を変換する処理を担っている "active_support/inflector" が読み込まれていないためです。

> [#] undefined method `singularize' for an instance of String

このため、Guardfile の先頭に以下のように require を追加します。

require "active_support/inflector"

guard :rspec, cmd: "bundle exec rspec" do

これでエラーが以下のように変わります。can_delete という fixture が存在しないためです。

No fixture named 'can_delete' found for fixture set 'articles'

上のテストで用意した article1 と can_delete の fixtures を準備することにします。

article1:
  title: "This is the first article"
  body: "This is the body of the first article"

can_delete:
  title: "This is the second article that can be deleted"
  body: "This is the body of the second article"

エラーは以下のようになりました。4つのテストのうち2つに失敗しています。

Article
  属性に関する共通テスト
    behaves like 存在制約
      is expected to be valid
      title の内容が nil のとき、エラーになること (FAILED - 1)
      body の内容が nil のとき、エラーになること (FAILED - 2)
    behaves like 削除可能制約
      削除できること

Failures:

  1) Article 属性に関する共通テスト behaves like 存在制約 title の内容が nil のとき、エラーになること
     Failure/Error: expect(subject).not_to be_valid
       expected #<Article id: 158250001, title: nil, body: "This is the body of the second article", created_at: "2024-01-23 20:50:13.877383000 +0900", updated_at: "2024-01-23 20:50:13.877383000 +0900"> not to be valid
     Shared Example Group: "存在制約" called from ./spec/models/article_spec.rb:10
     # ./spec/support/shared_example.rb:10:in `block (3 levels) in <main>'

  2) Article 属性に関する共通テスト behaves like 存在制約 body の内容が nil のとき、エラーになること
     Failure/Error: expect(subject).not_to be_valid
       expected #<Article id: 158250001, title: "This is the second article that can be deleted", body: nil, created_at: "2024-01-23 20:50:13.877383000 +0900", updated_at: "2024-01-23 20:50:13.877383000 +0900"> not to be valid
     Shared Example Group: "存在制約" called from ./spec/models/article_spec.rb:10
     # ./spec/support/shared_example.rb:10:in `block (3 levels) in <main>'

Finished in 0.04556 seconds (files took 1.54 seconds to load)
4 examples, 2 failures

このテストを通過させるために article.rb に validates を追加します。

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true
end

7.3.3 では body の最低文字数が 10 文字という制約をつけていました。これも実装の前にテストに追加しましょう。先ほどの属性に関する共通テストの中に追加します。境界値チェックということで、10文字で OK、9文字で NG であることを確認すればよいです。

    describe "body length check" do
      context "body is 10 characters" do
        before { subject.body = "a" * 10 }
        it { is_expected.to be_valid }
      end

      context "body is 9 characters" do
        before { subject.body = "a" * 9 }
        it { is_expected.to be_invalid }
      end
    end

当然ながらまだ制約を追加していないので、このような結果になりました。

1) Article 属性に関する共通テスト body length check body is 9 characters is expected to be invalid
     Failure/Error: it { is_expected.to be_invalid }
       expected `#<Article id: 158250001, title: "This is the second article that can be deleted", body: "aaaaaaaaa", ...reated_at: "2024-01-23 21:14:18.311106000 +0900", updated_at: "2024-01-23 21:14:18.311106000 +0900">.invalid?` to be truthy, got false
     # ./spec/models/article_spec.rb:21:in `block (5 levels) in <top (required)>'

このテストを通過させるために、実装を修正します。

  validates :body, presence: true, length: { minimum: 10 }

これでテストが通過しました。結果は以下のようになります。

Article
  属性に関する共通テスト
    behaves like 存在制約
      is expected to be valid
      title の内容が nil のとき、エラーになること
      body の内容が nil のとき、エラーになること
    behaves like 削除可能制約
      削除できること
    body length check
      body is 10 characters
        is expected to be valid
      body is 9 characters
        is expected to be invalid

Finished in 0.07829 seconds (files took 1.54 seconds to load)
6 examples, 0 failures

console で確認

せっかくなので console でエラーを確認してみましょう。 bin/rails c として Rails console を起動します。

$ bin/rails c
Loading development environment (Rails 7.1.2)
irb: warn: can't alias context from irb_context.
irb(main):001>

ここで title に ABC、body に DEF として作成してみます。

article = Article.new title: "ABC", body: "DEF"
=> #<Article:0x000000012acd9df0 id: nil, title: "ABC", body: "DEF", created_at: nil, updated_at: nil>

保存すると失敗しています。エラーメッセージを確認すると何でエラーが出たかがわかります。以前、locales/ja.yml を用意したため、メッセージが正しく日本語化されていました。便利ですね。

article.save
=> false
irb(main):003> article.errors.messages
=> {:body=>["は10文字以上で入力してください"]}

せっかくなので title も nil にして確認してみます。title の方にもエラーが追加されたのがわかります。

article.title = nil; article.save
=> false
article.errors.messages
=> {:title=>["を入力してください"], :body=>["は10文字以上で入力してください"]}

ついでに body も nil にしてみましょう。body は二つの validation に引っかかったことになるので、メッセージが配列になっていることがわかります。

article.body = nil; article.save
=> false
article.errors.messages
=> {:title=>["を入力してください"], :body=>["を入力してください", "は10文字以上で入力してください"]}

おわりに

今日は、モデルの属性に関するテストを実施しました。以前、locales を設定していたので、エラーメッセージが自動的に日本語化されていたことも確認できました。