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は、以下のような動きをする(その日から指定した日数後の日付が表示される)

Image from Gyazo

・sequenceを使うとtaskインスタンスを生成する度に、ユニーク制約に抵触しないように違う値が作られる
sequence(:title, "title_1")の場合(sequence(:カラム名, "カラムに投入するデータ")は、sequenceにブロックを渡さずに第二引数を渡している。
sequenceにブロックを渡さずに第二引数を渡している場合は、[.next]メソッドが呼ばれ末尾の数値を増やしてくれる。(形が、英字+記号+数字の場合)
sequenceに第二引数を渡して値が変わるのは、末尾の数字だけなので末尾の数字を変えたい場合は、ブロックが省略できるが、末尾以外の部分の数字を増やしたい場合は、ブロックが必要

Image from Gyazo

下記は、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

上記記述をしない場合

Image from Gyazo

上記記述をした場合

Image from Gyazo

参考記事:

スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編)

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

FactoryBotのassociationとは - Qiita

FactoryBotを使う時に覚えておきたい、たった5つのこと - Qiita

FactoryBotでテストデータ作成する方法 - そむりえエンジニアのブログ

モデルスペック#RSpec - Wataruの技術備忘録

FactoryBot (旧FactoryGirl) の sequence と .next - Qiita

FactoryBotについて|moeno|note

FactoryBotについて|moeno|note

【Rspec】「FactoryBot」の構文について | プログラミングマガジン

Factorybotを使ったテストデータの作成方法 - Qiita

Rspec,FactoryBotのsequence - Qiita

使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita

【RSpec】FactoryBotを使ってみよう【Rails】 - Qiita

RSpec の使い方 [Ruby] – Site-Builder.wiki