Chapter 12

3-2. User モデルのテスト

Masuyama
Masuyama
2022.10.15に更新

User モデルのテスト

前回、devise を用いて User モデルを作成しました。

実際に User モデルを使用する前に、User モデルが機能することを確認するためのテストを作成しておきましょう。

また、User モデルの一部の属性についてはバリデーションを設定しつつ、そのテストも同時に作成します。

テスト関連ファイルの確認

前回 rails g devise User を実行した際、テストに関連する以下のファイルが作成されていました。

  • spec/factories/users.rb
  • spec/models/user_spec.rb

今回はこれらを修正し、最後に bin/rspec 実行時に全てのテストが通ることを目標とします。

Factory ファイルの修正

spec/factories/users.rb は、以前導入した FactoryBot を用い、テストで User を簡単に作成するためのファイルです。

こちらを以下のように修正してください。

spec/factories/users.rb

FactoryBot.define do
  factory :user do
    nickname { 'テスト太郎' }
    sequence :email do |n|
      "test#{n}@example.com"
    end
    password { '111111' }
    password_confirmation { '111111' }
  end
end

ユニークキーである email の一部だけが異なる、複数の User を作成する Factory となっています。

User 生成&取得のテスト

次に User モデルの Spec であるspec/models/user_spec.rbを修正します。

まずは基本的なところとして、FactoryBot で生成した User を User.first で取得し、
取得した User が想定通りの属性(nickname, email) を持っていることを確認するテストを用意しましょう。

spec/models/user_spec.rb

require 'rails_helper'

describe User do
  let(:nickname) { 'テスト太郎' }
  let(:email) { 'test@example.com' }

  describe '.first' do
    before do
      create(:user, nickname: nickname, email: email)
    end

    subject { described_class.first }

    it '事前に作成した通りのUserを返す' do
      expect(subject.nickname).to eq('テスト太郎')
      expect(subject.email).to eq('test@example.com')
    end
  end
end

これでテストを実行してみます。

テストを実行する際、比較的時間がかかる System Spec まで一緒に実行すると開発スピードが落ちるため、User モデルの Spec だけを実行します。
特定のテストだけを実行したい場合は、bin/rspec の後に半角スペースを挟み、さらに実行したいテストのファイル名を渡します。

1 つだけですが、テストが通っていることを確認してください。

$ bin/rspec spec/models/user_spec.rb
...
User
  .first
    事前に作成した通りのUserを返す

Finished in 0.06006 seconds (files took 0.5011 seconds to load)
1 example, 0 failures

User.nickname のバリデーション(文字数)

ここでは User の nickname 属性に文字数制限のバリデーションを設定していきます。
その際、バリデーションのテストを作成し、それから実際にバリデーションを設定します。

※このように要件を満たすためのテストを事前に作成してから実装をすることをテスト駆動開発といいます。

バリデーションのテストを追加

今回は、ニックネームは 20 文字までという制限をかけていきます。

そこで、先にこのバリデーションに関するテストを作成しておき、それから実際に User モデルにバリデーションを設定します。
先にテストを書いておくことで仕様が明確になり、確実にバリデーションを実装することができます。

テストでは以下の 2 つを追加します。

  • User.nickname が 20 文字の場合、User オブジェクトが有効であること
  • User.nickname が 21 文字の場合、User オブジェクトが無効であること

これを満たすようなテストとして、.first という describe とはまた別に .validation という describe (分け方) のテストを追加します。

user_spec.rb の全文は以下のようになります。

spec/models/user_spec.rb

require 'rails_helper'

describe User do
  let(:nickname) { 'テスト太郎' }
  let(:email) { 'test@example.com' }
  let(:password) { '12345678' }
  let(:user) { User.new(nickname: nickname, email: email, password: password, password_confirmation: password) }

  describe '.first' do
    before do
      create(:user, nickname: nickname, email: email)
    end

    subject { described_class.first }

    it '事前に作成した通りのUserを返す' do
      expect(subject.nickname).to eq('テスト太郎')
      expect(subject.email).to eq('test@example.com')
    end
  end

  describe 'validation' do

    describe 'nickname属性' do

      describe '文字数制限の検証' do
        context 'nicknameが20文字以下の場合' do
          let(:nickname) { 'あいうえおかきくけこさしすせそたちつてと' } # 20文字

          it 'User オブジェクトは有効である' do
            expect(user.valid?).to be(true)
          end
        end

        context 'nicknameが20文字を超える場合' do
          let(:nickname) { 'あいうえおかきくけこさしすせそたちつてとな' } # 21文字

          it 'User オブジェクトは無効である' do
            user.valid?

            expect(user.valid?).to be(false)
            expect(user.errors[:nickname]).to include('is too long (maximum is 20 characters)')
          end
        end
      end
    end
  end
end

user という変数に作成した User オブジェクトを格納しています。

有効な user については user.valid?true となることを確認しています。

一方、無効な user についてはuser.valid?実行時にuser.errros にエラー内容が入ることを利用したテストとなっています。

まだバリデーションを設定はしていないので、テストは失敗することを確認してください。

$ bin/rspec spec/models/user_spec.rb
...
Finished in 0.11684 seconds (files took 0.50221 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./spec/models/user_spec.rb:35 # User validation nickname文字数制限の検証 nicknameが20文字を超える場合 User オブジェクトは無効である

バリデーションを設定しても通るテストについてはそのままですが、21 文字以上の nickname というテストは失敗しています。

バリデーションを追加

では User モデルにバリデーションを追加しましょう。

app/models/user.rb

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :nickname, length: { maximum: 20 } # 追加
end

nickname について、長さの最大が 20 文字であるという制約のバリデーションを追加しました。

テスト実行

バリデーションを追加したため、この時点でテストが通るはずです。

テストを再度実行し、User モデルのテストはすべて通ることを確認しましょう。

$ bin/rspec spec/models/user_spec.rb
...
User
  .first
    事前に作成した通りのUserを返す
  validation
    nickname属性
      文字数制限の検証
        nicknameが20文字以下の場合
          User オブジェクトは有効である
        nicknameが20文字を超える場合
          User オブジェクトは無効である

Finished in 0.11225 seconds (files took 0.4578 seconds to load)
3 examples, 0 failures

バリデーションを設定し、テストが通ることも確認できたので一旦コミットしておきます。

$ git add .
$ git commit -m "User.nickname の文字数に関するバリデーションを追加"

User.nickname のバリデーション(空欄ではないこと)

続いて、User.nickname は空欄ではないことのバリデーションを追加してあげます。

バリデーションのテストを追加

文字数のバリデーションと同じく、テストを先に追加しましょう。
nickname が空欄である User オブジェクトを作り、user.valid? をかけた時に空欄を許可しないことを示すエラーメッセージが含まれることを確認します。

spec/models/user_spec.rb

    describe 'nickname存在性の検証' do
      context 'nicknameが空欄の場合' do
        let(:nickname) { '' }

        it 'User オブジェクトは無効である' do
          expect(user.valid?).to be(false)
          expect(user.errors[:nickname]).to include("can't be blank")
        end
      end
    end

なお、上記テストの挿入位置ですが事前に追加していた describe '文字数制限の検証' do と同列となるように追記します。
これは、nickname 属性に関するいくつかの検証のうち「文字数制限」と「存在性」(空欄ではないこと) の検証が同列であることを明確にするためです。

こうしておくと、テストを実行した際にインデントが揃ったテストどうしは同列であることが分かるため、仕様やテストの意図が分かりやすくなります。

さて、このタイミングでは追加したテストだけが失敗することを確認しましょう。

$ bin/rspec spec/models/user_spec.rb
...
User
  .first
    事前に作成した通りのUserを返す
  validation
    nickname属性
      文字数制限の検証
        nicknameが20文字以下の場合
          User オブジェクトは有効である
        nicknameが20文字を超える場合
          User オブジェクトは無効である
      存在性の検証
        nicknameが空欄の場合
          User オブジェクトは無効である (FAILED - 1)
...
Failures:

  1) User validation nickname属性 存在性の検証 nicknameが空欄の場合 User オブジェクトは無効である
     Failure/Error: expect(user.valid?).to be(false)

       expected false
            got true
...

nickname が空欄であり、正しくない User オブジェクトについて user.valid? は本来 false を返すことをテストは期待しています。
しかし、そのバリデーションをかけていないため、まだ user.valid?true を返しています。
結果として、テストは現時点では失敗することなります。

テスト駆動開発では最初に「失敗するテスト」を書くことが重要です。
逆に、この時点でテストが通ってしまうようであれば、正しいテストを書けていませんので注意しましょう。

続けて、このテストが通るようにバリデーションを追加しましょう。

バリデーションを追加

テストが書けたので、nickname の空欄を強制するバリデーションを追加します。
presence: true を追加することで実現できます。

app/models/user.rb

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :nickname, length: { maximum: 20 }
  validates :nickname, presence: true # 追加
end

なお、本当は nickname に関するバリデーションは 1 行で書けるのですが、
テスト駆動開発の流れに則り最後にリファクタリングを行うため、あえて改善の余地があるコードのままにしておきます。

テスト実行

バリデーションを追加したので、今度はテストが通ることを確認してください。

$ bin/rspec spec/models/user_spec.rb
DEBUGGER: Attaching after process 50812 fork to child process 50861
Running via Spring preloader in process 50861

User
  .first
    事前に作成した通りのUserを返す
  validation
    nickname属性
      文字数制限の検証
        nicknameが20文字以下の場合
          User オブジェクトは有効である
        nicknameが20文字を超える場合
          User オブジェクトは無効である
      存在性の検証
        nicknameが空欄の場合
          User オブジェクトは無効である

Finished in 0.08105 seconds (files took 0.4687 seconds to load)
4 examples, 0 failures

テストのリファクタリング

さて、テストが通りましたのでリファクタリング(以下、リファクタ)をしていきましょう。

リファクタ作業では、処理による最終的な結果を変えることなく重複を省いたり、設計を変えたりしてコードの改善を行います。

テスト駆動開発では

  1. 失敗するテストを書く
  2. 実装してテストを通す
  3. リファクタをしてコードを改善する

という流れで実装を進めていき、品質を高めていきます。

では実際にリファクタをしていきましょう。
User モデルのコードで nickname のバリデーションはわざわざ 2 行に分けていましたが、
先ほど軽く触れた通り 1 行でまとめられるのでリファクタしてしまいます。

app/models/user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :nickname, presence: true, length: { maximum: 20 } # 変更
end

裏ファクタを終えたので最後にテストを再実行し、すべて通ることを確認します。

$ bin/rspec spec/models/user_spec.rb
...
4 examples, 0 failures

「テストを通している=動作の保証ができている」という流れで進めていたため、
テストさえ通るようにすれば、安心してリファクタリングできますね!

これで一通りのテスト駆動開発の流れは掴めたかと思います。

では最後にすべてのテストを走らせ、コケているテストが無いことを確認しておきましょう。
開発を進めていくうちに予期せぬ箇所に影響することもあるため、最終的にはすべてのテストを走らせておくことが重要です。

$ bin/rspec
...
Finished in 2.61 seconds (files took 0.34742 seconds to load)
6 examples, 0 failures

変更をコミット

問題ないことを確認できたら、ここまでの変更をコミットし、プッシュもしておきます。

$ git add .
$ git commit -m "User.nicknameのバリデーションを追加"
$ git push

補足

テストコードの全文

今回はテストコードについて何度か修正を行ったため、全貌を掴むのに苦労したかと思います。
念のためテストの全文を載せておきますので、自分のテストと照らし合わせておきましょう。

spec/models/user_spec.rb

require 'rails_helper'

describe User do
  let(:nickname) { 'テスト太郎' }
  let(:email) { 'test@example.com' }
  let(:password) { '12345678' }
  let(:user) { User.new(nickname: nickname, email: email, password: password, password_confirmation: password) } # 変数に格納

  describe '.first' do
    before do
      create(:user, nickname: nickname, email: email)
    end

    subject { described_class.first }

    it '事前に作成した通りのUserを返す' do
      expect(subject.nickname).to eq('テスト太郎')
      expect(subject.email).to eq('test@example.com')
    end
  end

  describe 'validation' do

    describe 'nickname属性' do
      describe '文字数制限の検証' do
        context 'nicknameが20文字以下の場合' do
          let(:nickname) { 'あいうえおかきくけこさしすせそたちつてと' } # 20文字

          it 'User オブジェクトは有効である' do
            expect(user.valid?).to be(true)
          end
        end

        context 'nicknameが20文字を超える場合' do
          let(:nickname) { 'あいうえおかきくけこさしすせそたちつてとな' } # 21文字

          it 'User オブジェクトは無効である' do
            user.valid?

            expect(user.valid?).to be(false)
            expect(user.errors[:nickname]).to include('is too long (maximum is 20 characters)')
          end
        end
      end

      describe '存在性の検証' do
        context 'nicknameが空欄の場合' do
          let(:nickname) { '' }

          it 'User オブジェクトは無効である' do
            expect(user.valid?).to be(false)
            expect(user.errors[:nickname]).to include("can't be blank")
          end
        end
      end
    end
  end
end

email / password のバリデーションについて

nickname についてのみバリデーションを設定しましたが、email や password のバリデーションについても気になる方がいるかと思います。

しかし、devise で最初から用意されているカラムについては最低限のバリデーションが設定されているため、
存在性(空欄ではないこと)のような基本的なバリデーションは不要です。

実際、email を空欄にした User を作成するようなテストを書いてみると "Email can't be blank" と出力されます。

     Failure/Error: create(:user, nickname: '太郎', email: '')

     ActiveRecord::RecordInvalid:
       Validation failed: Email can't be blank

このように gem で最初から用意されているメソッド等については
既に gem 側でテストが書かれているため、新たにテストを書かないのが一般的です。

※バリデーションメッセージが画面に表示されるという要件については、今後作っていく System Spec にてテストします。

宿題

RSpec の書き方おさらい

以前、RSpec の章で基本的な RSpec の書き方については主に宿題で学んでいただきましたが、
今回使用したいくつかの書き方は個別に復習しておきましょう。

バリデーション

Rails でモデルに制約をかけることをバリデーションといいますが、もしご存じではない方はこの機会に学んでおきましょう。

テスト駆動開発

今回、失敗するテストを書く => テストを通す => テストをリファクタする、というテスト駆動開発の流れで進めました。

さらっと流れだけを説明しましたが、その考え方自体は大変奥深いものです。
テストを書くことの重要性をご理解いただく意味でも、テスト駆動開発の概念や経緯については一度目を通しておくと勉強になります。