【RSpec】モデルスペックの基本

## はじめに
こんにちは、@jinta_02と申します。
個人開発をするにあたり、モデルスペックを書く機会があったので、記録として記事を書きます。
完全未経験者の記事のため、誤りがございましたらコメント等いただけますと幸いです🙇‍♂️

## 前提
Ruby: 3.2.2
Rails: 7.0.6
rspec-rails: 6.0.3

## モデルスペックとは
RSpecを使用して、Railsアプリケーション内のデータモデル（ActieRecordモデル）をテストするためのスペックの一種。データモデルの振る舞いやバリデーション、データベースの操作などをテストし、モデルの正確性を確認するためのものです。
### 具体的には以下のようなテストを行います。
- **バリデーションのテスト**
モデルが正しいバリデーションを持っているかを確認します。
- **関連付け（アソシエーション）のテスト**
`belong_to`, `has_many`などの関連付けが適切に動いているかを確認します。
- **カスタムメソッドのテスト**
モデルに定義されたカスタムメソッドが正しく動作しているかを確認します。
- **コールバックのテスト**
モデルのコールバック（`before_action`, `before_save`）が適切に動作しているかを確認します。
- **データベースの操作テスト**
データベースに正しく保存、更新、削除できるかを確認できます。

<br>

今回は、ユーザーモデルに関するモデルスペックを書いていきたいと思います。

## 事前準備（ダミーデータの作成）
事前準備として、FactoryBotでダミーデータを定義しておきます。
```Ruby:spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:name) { |n| "user_#{n}" }
    sequence(:email) { |n| "user_#{n}@example.com" }
    password { "password" }
    password_confirmation { "password" }
    role { "general" }
  end
end
```
ダミーデータを定義しておくことで、以下のように簡単にインスタンスを作成したり、保存できたりします。
```ruby:spec/models/〇〇_spec.rb
# ユーザーのインスタンスを作成。
user = FactoryBot.build(:user)
# ユーザーのインスタンスを作成し、データベースへ保存する。
user = FactoryBot.create(:user)
```
## 今回のモデルの確認
```Ruby:app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  # ユーザーが作成されたら実行する
  after_create :create_initial_list

  # ユーザーが複数のリストを持てるよう関連付け（１対多）
  has_many :shop_saved_lists, dependent: :destroy
  # ユーザーが複数のレビューを書けるよう関連付け（１対多）
  has_many :reviews, dependent: :destroy

  # 一般ユーザーと管理者を区別するようenumの定義
  enum role: { general: 0, admin: 1 }

  validates :name, presence: true, length: { maximum: 255 }

  # ユーザーが作成されたら、リスト（'お気に入り'）を作成するメソッド
  def create_initial_list
    shop_saved_lists.create(name: 'お気に入り')
  end

  # 自身のオブジェクトかを確認するメソッド
  def own?(object)
    id == object.user_id
  end
end
```

## バリデーションに関するテスト

### 1. モデルのどの部分？
```Ruby:app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :name, presence: true, length: { maximum: 255 }
end
```
今回は、gem 'devise'を使用してユーザー操作を実装したため、deviseの`:validatable`が定義されています。

### 2. 実際に書いてみる
```Ruby:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'ユーザーのバリデーションに関するテスト' do
    it '名前、メールアドレス、パスワードがある場合は有効' do
      # 有効なユーザーの作成
      valid_user = FactoryBot.build(:user)
      # 作成したユーザーが有効かを確認
      expect(valid_user).to be_valid
    end

    it '名前がない場合は無効' do
      # 名前が空欄のユーザーを作成
      user_without_name = FactoryBot.build(:user, name: '')
      # 作成したユーザーが無効かを確認
      expect(user_without_name).to be_invalid
    end

    it '名前が255文字を超える場合は無効' do
      # 名前が256文字のユーザーを作成
      user_more_than_name = FactoryBot.build(:user, name: 
      'a' * 256)
      # 作成したユーザーが無効かを確認
      expect(user_more_than_name).to be_invalid
    end
    
    it 'メールアドレスがない場合は無効' do
      # メールアドレスが空欄のユーザーを作成
      user_without_email = FactoryBot.build(:user, email: '')
      # 作成したユーザーが無効かを確認
      expect(user_without_email).to be_invalid
    end
    
    it  '重複したメールアドレスは無効' do
      # ユーザーを作成し、データベースへ保存
      user = FactoryBot.create(:user)
      # 保存したユーザーと同じメールアドレスを持ったユーザーを新たに作成
      user_duplicate_email = FactoryBot.build(:user, email: user.email)
      # 新たに作成したユーザーが無効かを確認
      expect(user_duplicate_email).to be_invalid
    end
    
    it 'パスワードがない場合は無効' do
      # パスワードが空欄のユーザーを作成
      user_without_password = FactoryBot.build(:user, password: '')
      # 作成したユーザーが無効かを確認
      expect(user_without_password).to be_invalid
    end
    
    it 'パスワード（確認用)とパスワードが一致しない場合は無効' do
      # パスワードが一致しないユーザーを作成
      user_password_mismatch = FactoryBot.build(:user, password_confirmation: 'mismatch')
      # 作成したユーザーが無効かを確認
      expect(user_password_mismatch).to be_invalid
    end
  end
end
```

## 関連付け（アソシエーション）に関するテスト
### 1. モデルのどの部分？
```Ruby:app/models/user.rb
class User < ApplicationRecord
  # ユーザーが複数のリストを持てるよう関連付け（１対多）
  has_many :shop_saved_lists, dependent: :destroy
  # ユーザーが複数のレビューを書けるよう関連付け（１対多）
  has_many :reviews, dependent: :destroy
end
```

### 2. 実際に書いてみる
```Ruby:spec/models/user_spce.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'ユーザーのバリデーションに関するテスト' do
    ...
  end

  describe 'ユーザーの関連付けに関するテスト' do
    # 各テストで使用するので事前に作成し、データベースへ保存
    before do
      @user = FactoryBot.create(:user)
    end

    it 'shop_saved_listsとの関連付け正しく設定されていること' do
      # ユーザーと関連付けされたリストを作成
      shop_saved_list = FactoryBot.create(:shop_saved_list, user: @user)
      # 「@user.shop_saved_lists」を実行したときに、作成したリストを含んでいるかを確認
      expect(@user.shop_saved_lists).to include shop_saved_list
    end

    it 'reviewsとの関連付けが正しく設定されていること' do
      #ユーザーと関連付けされたレビューを作成
      review = FactoryBot.create(:review, user: @user)
      # 「@user.reviews」を実行した時に、作成したレビューを含んでいるかを確認
      expect(@user.reviews).to include review
    end
  end
end
```

## enum定義に関するテスト
### 1. モデルのどの部分？
```Ruby:app/models/user.rb
class User < ApplicationRecord
  # 一般ユーザーと管理者を区別するようenumの定義
  enum role: { general: 0, admin: 1 }
end
```

### 2. 実際に書いてみる
```Ruby:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'ユーザーのバリデーションに関するテスト' do
	...
  end

  describe 'ユーザーの関連付けに関するテスト' do
    ....
  end

  describe 'ユーザーのenum値に関するテスト' do
    it '一般ユーザーが正しいenum値を持つこと' do
      # ユーザーを作成し、データベースへ保存
      user = FactoryBot.create(:user)
      # 作成したユーザーがroleカラムで、'general'を持っているかを確認
      expect(user.role).to eq 'general'
    end
  end
end
```

## インスタンスメソッドに関するテスト
### 1. モデルのどの部分？
```Ruby:app/models/user.rb
class User < ApplicationRecord
  # ユーザーが作成されたら、リスト（'お気に入り'）を作成するメソッド
  def create_initial_list
    shop_saved_lists.create(name: 'お気に入り')
  end

  # 自身のオブジェクトかを確認するメソッド
  def own?(object)
    id == object.user_id
  end
end
```

### 2. 実際に書いてみる
```Ruby:spec/models/user_spec.rb
# spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'ユーザーのバリデーションに関するテスト' do
	...
  end

  describe 'ユーザーの関連付けに関するテスト' do
    ...
  end

  describe 'ユーザーのenum値に関するテスト' do
	..
  end

  describe 'create_initial_listメソッドに関するテスト' do
    it 'ユーザーが作成された時に初期リストが作成されること' do
      # ユーザーを作成し、データベースへ保存
      user = FactoryBot.create(:user)
      # 作成したユーザーがすでに、「'お気に入り'」のリストを保有しているかを確認
      expect(user.shop_saved_lists.where(name: 'お気に入り')).to exist
    end
  end

  describe 'own?メソッドに関するテスト' do
    # 各テストで使用するので、事前に作成し、データベースへ保存
    before do
      @user = FactoryBot.create(:user)
    end

    it 'ユーザーがオブジェクトの所有者である場合は、trueを返すこと' do
      # ユーザーと関連付けされたレビューを作成
      review = FactoryBot.create(:review, user: @user)
      # ユーザーが作成したレビューかを確認
      expect(@user.own?(review)).to eq(true)
    end

    it 'ユーザーがオブジェクトの所有者でない場合は、falseを返すこと' do
      # もう一人のユーザーを作成し、データベースへ保存
      another_user = FactoryBot.create(:user)
      # もう一人のユーザーと関連付けされたレビューを作成
      review = FactoryBot.create(:review, user: another_user)
      # ユーザーが作成したレビューではないことを確認
      expect(@user.own?(review)).to eq(false)
    end
  end
end
```

## テストを実行
最後にテストを実行して、正常にテストが実行されているかを確認します。
テストを実行するコマンド以下の通りです。
```shell
$ bundle exec rspec spec/models/user_spec.rb
```
上記のコマンドは、特定のファイルを指定して、テストを実行します。
modelスペック全てのテストを実行する場合は、以下のように実行します。
```shell
$ bundle exec rspec spec/models
```
`user_spec.rb`のテスト実行結果が以下のようになっていれば、正常です。
![e0da27e2f1af5f30585413358528875e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2600084/0832cc9d-2773-8914-443c-90698e05ef39.png)

## 最後に
今回は、個人開発を終えた後にまとめてモデルスペックを書いたため、かなり根気のいる作業になりました。
今後の開発では、テスト駆動開発（TDD）を導入して、最初にテストを書いてからコードを実装する方が良いなと感じました。 
それでは以上になります。 
最後までご覧いただき、ありがとうございました🙇‍♂️

## 参考文献
- https://qiita.com/a-kym/items/1b7b4af1e6d102625562