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の基本的な構文や便利な機能を理解する」
※間違いなどあればコメントください!