RSpecを使ったバリデーションテスト
RSpecを使ったバリデーションテスト
下記のようなスキーマ構造とバリデーションが設定されているRSpecを用いたモデルのバリデーションテスト
[app/models/task.rb] 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
[app/models/user.rb] 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 def my_object?(object) object.user_id == id end end
[db/schema.rb] ActiveRecord::Schema.define(version: 2019_10_07_091857) do create_table "tasks", force: :cascade do |t| t.string "title" t.text "content" t.integer "status" t.datetime "deadline" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "user_id" t.index ["user_id"], name: "index_tasks_on_user_id" end create_table "users", force: :cascade do |t| t.string "email", null: false t.string "crypted_password" t.string "salt" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["email"], name: "index_users_on_email", unique: true end end
手順を以下に説明します
FactoryBotでテストデータの形式を設定する
下記は、taskのテストデータ
[spec/factories/tasks.rb] FactoryBot.define do factory :task do # taskという名前のインスタンス(taskモデルなので名前をモデル名と一緒にする) sequence(:title, "title_1") # ユニーク制約に抵触しないようにシーケンスを使う content {"content"} # カラム名{"カラムに投入するデータ"}の形式になっている status { :todo } deadline { 1.week.from_now } association :user # associationを設定することにより、taskのインスタンスを作成する時に自動でuserインスタンスも作成して外部キーにuserの値を入れてくれる(taskインスタンスを作成する前にuserを作成する必要がない) end end
※ポイント
・factory :task doの部分は、生成するインスタンスの名前をモデル名と一緒の名前にするが、モデル名と違う名前のインスタンス名にしたい時は、[factory :other_task, class: Task do]のようにする
・week.from_nowは、以下のような動きをする(その日から指定した日数後の日付が表示される)
・sequenceを使うとtaskインスタンスを生成する度に、ユニーク制約に抵触しないように違う値が作られる
sequence(:title, "title_1")の場合(sequence(:カラム名, "カラムに投入するデータ")は、sequenceにブロックを渡さずに第二引数を渡している。
sequenceにブロックを渡さずに第二引数を渡している場合は、[.next]メソッドが呼ばれ末尾の数値を増やしてくれる。(形が、英字+記号+数字の場合)
sequenceに第二引数を渡して値が変わるのは、末尾の数字だけなので末尾の数字を変えたい場合は、ブロックが省略できるが、末尾以外の部分の数字を増やしたい場合は、ブロックが必要
下記は、userのテストデータ
[spec/factories/users.rb] FactoryBot.define do factory :user do sequence(:email) { |n| "user_#{n}@example.com"} # ユニーク制約に抵触しないようにシーケンスを使う<br> password { "password" } password_confirmation { "password" } end end
※ポイント
・[sequence(:email) { |n| "user_#{n}@example.com"}]は、下記と一緒
sequence(:email) do |n| "user_#{n}@example.com" end
上記は、メールアドレスの[n]の部分がuserが作成される度に数値が増えていくので、ユニーク制約に抵触しない
↓
下記を設定することにより、FactoryBotの記述を省ける
[spec/rails_helper.rb] config.include FactoryBot::Syntax::Methods
上記設定をしなかった場合は、このように記述しなければいけない user = FactoryBot.create(:user) 上記設定をしたら下記のように省略して記述できる user = create(:user)
↓
下記がバリデーションのテストコード
[spec/models/task_spec.rb] require 'rails_helper' RSpec.describe Task, type: :model do describe 'validation' do it 'is valid with all attributes' do # 全ての属性が存在する場合 task = build(:task) # taskインスタンスを作成(DBには、保存していない) expect(task).to be_valid # be_validは、valid?をマッチャにしている expect(task.errors).to be_empty end it 'is invalid without title' do # タイトルが無い場合 task_without_title = build(:task, title: "") # インスタンスを作成する時に、カラムを渡すことでFactoryBotの内容をオーバーライドできる expect(task_without_title).to be_invalid expect(task_without_title.errors[:title]).to eq ["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 eq ["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) # 上記のtaskと同じタイトルのインスタンスを生成 expect(task_with_duplicated_title).to be_invalid expect(task_with_duplicated_title.errors[:title]).to eq ["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
※ポイント
・[build]メソッド: [build]は、インスタンスを生成しただけでDBには保存していない
・[create]メソッド: [create]は、インスタンスを生成してDBに保存する
・[be○○]マッチャ: [valid?]メソッドや[empty?]メソッドなどのように、最後が[?]になっていて、[true]もしくは[false]を返すメソッドは、[be○○]のようにして使える(このマッチャがtrueになったらテストが通過する)
・[task_without_title = build(:task, title: "") ]のように記述することで、FactoryBotのカラム内容をオーバーライドしてインスタンスを生成できる
・[.rspec]ファイルに[--format documentation]を下記のように記述するとテスト実行時にメッセージが表示される
[.rspec] --format documentation
上記記述をしない場合
上記記述をした場合
参考記事:
スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編)
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita
FactoryBotのassociationとは - Qiita
FactoryBotを使う時に覚えておきたい、たった5つのこと - Qiita
FactoryBotでテストデータ作成する方法 - そむりえエンジニアのブログ
FactoryBot (旧FactoryGirl) の sequence と .next - Qiita
【Rspec】「FactoryBot」の構文について | プログラミングマガジン
Factorybotを使ったテストデータの作成方法 - Qiita
Rspec,FactoryBotのsequence - Qiita
使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita