RSpecでバリデーションのテストを書こう!【rails】

はじめに

先日、はじめてRSpecでバリデーションのテストを書いたのでアウトプットとして書きます。 ご参考になれば幸いです。

userモデルとtaskモデル

userモデルとtaskモデルはこんな感じです。

※今回はtaskモデルのバリデーションテストのみ書きます。

class User < ApplicationRecord
  authenticates_with_sorcery!

  has_many :tasks, dependent: :destroy

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true, presence: true
end
class Task < ApplicationRecord
  belongs_to :user
  validates :title, presence: true, uniqueness: true
  validates :status, presence: true
  enum status: { todo: 0, doing: 1, done: 2 }
end

モデルスペックの作成

まずは下記コマンドで、userとtaskのモデルスペックを作成します。

$ bundle exec rails g rspec:model user

$ bundle exec rails g rspec:model task

FactoryBotをインストールしているので、上記コマンドを実行すると それぞれのスペックファイルとファクトリが作成されます。

テストデータを作成

先程作成したファクトリーにuserとtaskのテストデータを定義します。

FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "test#{n}@example.com" }
    password { "password" }
    password_confirmation { "password" }
  end
end
FactoryBot.define do
  factory :task do
    sequence(:title, "title_1")
    content { "content" }
    status { :todo }
    deadline { 1.week.from_now }
    association :user
  end
end

上記のように定義することで、

例えば、テストコードで「FactoryBot.create(:user)」とすると、定義された値の入ったuserインスタンスが作成されます。

RSpecの基本構文

テストコードを書く前に、RSpecの基本構文をざっくりと説明します!

ほんとにざっくりと。。。笑

とりあえず大まかに下記のような感じ

RSpec.describe 'グループ化を宣言' do
  describe 'さらに細分化' do
    it '期待するテスト' do ※itではなくexampleでも可
      expect('X').to eq 'Y'
    end
  end
end

値を当てはめるとこんな感じ↓

RSpec.describe '四則演算' do
  describe '足し算' do
    it '1 + 1 は 2 になること' do
      expect(1 + 1).to eq 2
    end
  end
end

まず一行目で「四則演算のテストですよ」とグループ化を宣言。

二行目では「足し算について」とさらに細分化。

三行目で期待するテストについて記載し、 itの中にネストして、expectを使ってテストコードを書いていく。

バリデーションのテストコードの構成

バリデーションがしっかりと機能しているのかを確認するために、 成功時と失敗時のテストを書きます。

つまり、有効な値をいれてバリデーションに通るテストと、 バリデーションにわざと引っかかって失敗するテストをそれぞれ書きます。

具体的な構成はこんな感じ↓

RSpec.describe Task, type: :model do
  describe 'validation' do
    it 'is valid with all attributes' do end
    it 'is invalid without title' do end 
    it 'is invalid without status' do end 
    it 'is invalid with a duplicate title' do end
    it 'is valid with another title' do end
  end
end

テストコードを作成

ではいよいよテストコードを書いていきます。

 1.バリデーションに通るテスト

    it 'is valid with all attributes' do
      task = build(:task) ※buildはnewメソッドと同じ感じ
      expect(task).to be_valid
      expect(task.errors).to be_empty
    end

まず、FactoryBotで定義した値の入っているtaskインスタンスをローカル変数taskに代入します。

そしてその変数taskに対して、「be_valid」というマッチャを使い、taskインスタンスが有効であることを確認しています。

最後に、errorsメソッドを使ったtaskに対して、「be_empty」というマッチャを使い、エラーメッセージが空であることを確認しています。

 2.titleカラムが空のインスタンスは、作成できないことを確認するテスト

    it 'is invalid without title' do
      task_without_title = build(:task, title: nil) ※第二引数でtitleカラムだけ上書き
      expect(task_without_title).to be_invalid
      expect(task_without_title.errors[:title]).to include("can't be blank")
    end

まず、タイトルが空のtaskインスタンスを作成し、ローカル変数に代入します。

変数名は何が代入されているのか理解しやすくするために「task_without_title」とします。

続いて、定義したローカル変数に対して、「be_invalid」というマッチャを使い、無効なインスタンスであることを確認します。

そして最後に、titleカラムに対するエラーメッセージを取り出し、「include」マッチャを使い、"can't be blank"という文字列が含まれていることを確認しています。

statusカラムのテストもほぼ同じなので載せておきます。

    it 'is invalid without status' do
      task_without_status = build(:task, status: nil)
      expect(task_without_status).to be_invalid
      expect(task_without_status.errors[:status]).to include("can't be blank")
    end

 3.重複したtitleは作成できないことを確認するテスト

    it 'is invalid with a duplicate title' do
      task = create(:task)   ※データベースに保存
      task_with_duplicated_title = build(:task, title: task.title)
      expect(task_with_duplicated_title).to be_invalid
      expect(task_with_duplicated_title.errors[:title]).to include("has already been taken")
    end

まず、createメソッドでtaskインスタンスを作成し、ローカル変数taskに代入します。

そして今度は、buildメソッドでtitleが先程作成したtaskインスタンスとまったく同じtitleの、taskインスタンスをローカル変数「task_with_duplicated_title」に代入します。

以下2行は、先程説明した通りです。

 4.重複していないtitleならば作成できることを確認するテスト

    it 'is valid with another title' do
      task = create(:task)
      task_with_another_title = build(:task, title: "another_title") ※第二引数は必須ではないがわかりやすいので記入
      expect(task_with_another_title).to be_valid
      expect(task_with_another_title.errors).to be_empty
    end

まず有効なtaskインスタンスを作成します。

続いて、titleが重複していないtaskインスタンスをローカル変数「task_with_another_title」に代入します。

以下2行は、先程説明した通りです。

完成したコード

require 'rails_helper'

RSpec.describe Task, type: :model do
  describe 'validation' do
    it 'is valid with all attributes' do
      task = build(:task)
      expect(task).to be_valid
      expect(task.errors).to be_empty
    end
    
    it 'is invalid without title' do
      task_without_title = build(:task, title: nil)
      expect(task_without_title).to be_invalid
      expect(task_without_title.errors[:title]).to include("can't be blank")
    end

    it 'is invalid without status' do
      task_without_status = build(:task, status: nil)
      expect(task_without_status).to be_invalid
      expect(task_without_status.errors[:status]).to include("can't be blank")
    end

    it 'is invalid with a duplicate title' do
      task = create(:task)
      task_with_duplicated_title = build(:task, title: task.title)
      expect(task_with_duplicated_title).to be_invalid
      expect(task_with_duplicated_title.errors[:title]).to include("has already been taken")
    end

    it 'is valid with another title' do
      task = create(:task)
      task_with_another_title = build(:task, title: "another_title")
      expect(task_with_another_title).to be_valid
      expect(task_with_another_title.errors).to be_empty
    end
  end
end

参考

RSpecとFactoryBotのインストール【rails】

Everyday Rails - RSpecによるRailsテスト入門

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」

※間違いなどあればコメントください!