Programming Journal

学習したことの整理用です。

【RSpec】 管理者・一般ユーザーを分けてテストデータを作成する

実装したいこと

管理者と一般ユーザーで権限が違うため、それぞれのテストデータを作成したい。

trait

トレイトを使う。 属性を予めセットして定義できる機能。

trait:A distinguishing quality or characteristic, typically one belonging to a person.

FactoryBot.define do
  factory :user do
    sequence(:name) { |n| "general-#{n}" } #sequenceで一意のデータを生成する。
    password { 'password' }
    password_confirmation { 'password' }
    role { :general } #default値を設定する。

    trait :admin do
      sequence(:name) { |n| "admin-#{n}" }
      role{ :admin }
    end

    trait :general do
      sequence(:name) { |n| "general-#{n}" }
      role{ :general }
    end
  end
end

traitの使い方がいまいち理解できてなかったのですが、
デフォルトでは全ての属性をセットしておく。→ traitは指定したい属性だけ切り取ってセットする。
っていうことですね。

マクロを使ってログイン機能だけセットする

module LoginMacro
  def login_as(user)
    visit login_path
    fill_in 'user[name]', with: user.name

    click_button '次へ'
    fill_in 'user[password]', with: 'password' #user.passwordはnil
    click_button 'ログイン'
  end
end

rails_helper.rbで読み込み設定をしておきます。

(略)
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } #support以下を読み込む

RSpec.configure do |config|
(略)
  config.include FactoryBot::Syntax::Methods #FacctoryBotの記述を省略できる
  config.include LoginMacros #LoginMacrosを読み込む
end

module ? macros?

以前もログイン機能の切り出しはしたのですが、そのときはLoginModuleとしていました。
Macro Moduleどっち?…と疑問に思ったので講師の方に質問したところ、慣例で現場によって異なるのでどっちでもいいとのことでした。

System Spec

require 'rails_helper'

RSpec.describe "AdiminArticlesPreviews", type: :system do
  let(:admin) { create :user, :admin }
  describe '記事作成画面で画像ブロックを追加' do
    context '画像なしで投稿する' do
      it 'プレビューが正常に表示される' do
        login_as(admin)
    (略)
        switch_to_window(windows.last) #windowを切り替え、最後のwindowを指定
        expect(page).to have_content('test')
      end
    end
  end
end

let(:admin) { create :user, :admin }
let(定義名) { 内容 }です。
{ create :user, :admin }:admin部分で、さっき定義したtraitを使っています。


login_as(admin)
マクロで定義したログイン用のメソッドを使っています。
引数に、letで作成したadminを渡して、管理者としてログインしています。

System Specの命名について

Controllerが、admin/articles/previews_controllerのように名前空間で分かれているとき、どうやってファイル名をつけたらいいのかな?って思ったけど、普通に、admin_articles_previews_spec.rbとするみたい。

NGコード

最初に私が実装したコードを反省用に残しておく。一応動くけど酷い、汚い。。
describe,contextbeforeをなんとなくで使っているので勉強します

require 'rails_helper'

RSpec.describe "Previews", type: :system do
  let(:admin) { create(:user, :admin) }
  before{ login_as(admin) }

  describe '記事の画像投稿機能' do
    before do
      click_link '記事'
      click_link '新規作成'
      fill_in 'タイトル',  with: 'test'
      click_button '登録する'
      click_link 'ブロックを追加する'
      click_link '画像'
    end

    context '画像なしで投稿する' do
      it 'プレビューが見れること' do
        click_link 'プレビュー'
        switch_to_window(windows.last)
        expect(page).to have_content('test')
      end
    end
  end
end

参考

トレイトについて 『everydayRails』P64

マクロについて
マクロ(ヘルパーメソッド)を定義してフィーチャースペックのユーザー切替えを楽に行う - Qiita

【RSpec】gem Seed Fu

gem Seed Fu

Seed Fu is an attempt to once and for all solve the problem of inserting and maintaining seed data in a database. It uses a variety of techniques gathered from various places around the web and combines them to create what is hopefully the most robust seed data system around.
GitHub - mbleigh/seed-fu: Advanced seed data handling for Rails, combining the best practices of several methods together.

シードを挿入してくれるgem

テスト環境にシードデータを投入するのに、以下のコマンドを実行していました。

rails db:seed_fu RAILS_ENV=test

こっちでもいい

RSpec.configure do |config|
(略)
  config.before :suite do
    SeedFu.seed
  end
end

before(:suite)はテスト実行前に実行される。
(『everydayRails』p42,171)

SeedFu.seed ってどういう意味??と思ったら、公式READ.MEの中に、以下の一文があった。

You can also do a similar thing in your code by calling SeedFu.seed(fixture_paths, filter).

参考

railsで初期データを入れる(seed-fuの使い方) - Qiita

SeedFu.seedのコードリーディングはこちらを参考にした。
seed-fuコードリーディング

【Rails】雑多なメモ

知らないメソッドが色々ありすぎて、忘れそうなのでメモしていきます。

protect_from_forgery with: :exception

# Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

protect_from_forgery | Railsドキュメント

protect_from_forgery with: :exception
このコードがあると、Railsで生成されるすべてのフォームとAjaxリクエストにセキュリティトークンが自動的に含まれます。セキュリティトークンがマッチしない場合には例外がスローされます。

クロスサイトリクエストフォージェリ (CSRF)
この攻撃方法は、ユーザーによる認証が完了したと考えられるWebアプリケーションのページに、悪意のあるコードやリンクを仕込むというものです。そのWebアプリケーションへのセッションがタイムアウトしていなければ、攻撃者は本来認証されていないはずのコマンドを実行できてしまいます。
Rails セキュリティガイド - Railsガイド

helper_method

  def current_site
    @current_site ||= Site.first
  end
  helper_method :current_site

helper_method (AbstractController::Helpers::ClassMethods) - APIdock

コントローラー内のメソッドをヘルパーとして宣言する。
viewでもこのメソッドが使えるようになる。

gem pundit

認可に使用する。
ユーザーによってできることを限定したり、許可したりするのに使う。

Pundit provides a set of helpers which guide you in leveraging regular Ruby classes and object oriented design patterns to build a simple, robust and scaleable authorization system.
File: README — Documentation for pundit (2.1.0)

class ApplicationController < ActionController::Base
  include Pundit

(略)
end

Pundit is focused around the notion of policy classes. We suggest that you put these classes in app/policies. This is a simple example that allows updating a post if the user is an admin, or if the post is unpublished:

class PostPolicy
  attr_reader :user, :post

  def initialize(user, post)
    @user = user
    @post = post
  end

  def update?
    user.admin? or not post.published?
  end
end

slug

位置を示すコード
URLの末尾
ユーザーフレンドリーになるように、URLを文字列型にする。
以下のstackoverflowの回答が分りやすかった。

It's simply a way to create user-friendly URLs:

stackoverflow.com

RailsのURLをID以外から付ける - Qiita

【Rails】ArgumentError - Nil location provided. Can't build URI エラーが出たら

エラーで悩んだのでメモ
画像を投稿するページで、画像を選択しないでプレビュー画面を開こうとすると以下のエラーになりました。

エラー画面

ArgumentError - Nil location provided. Can't build URI

f:id:Study-Diary:20201002135409p:plain
ArgumentError

ArgumentError - Nil location provided. Can't build URI.:
  app/views/shared/_media_image.html.slim:5:in `_app_views_shared__media_image_html_slim___195936332176276977_70365205560160'
  app/models/article.rb:76:in `block in build_body'
  app/models/article.rb:70:in `build_body'
  app/controllers/admin/articles/previews_controller.rb:9:in `show'

View修正

画像がなかった場合を想定していないコードだったので、エラーが出てしまいました。
改善方法は2つありますが、今回は①を選択しました。
image_tagがあるときだけrenderされる
image_urlにデフォルト値を設定しておく

#①
- if medium.image_url #追加
  .media-image
    = image_tag medium.image_url(:lg)

#②
= image_tag medium.image_url || default_image 

※最初、ifを中に入れ込んでしまったけど、これだと、elseのときにmedia-imageに空タグが入ってしまうのでNG

.media-image
  - if medium.image_url
    = image_tag medium.image_url(:lg)

参考

Argument Error Nil location provided. Can't build URI for an image_tag using carrier wave Ruby on Rails - Stack Overflow

ArgumentError Nil location provided. Can't build URI. が出た時の対処法 プロフ画像の実装 | びんぼーろく

【RSpec】System Spec

今回の記事の目的

System Specのコードを書いていて迷うことがいくつかあったので、復習していきます。
前回の記事同様、RSpec,FactoryBot and Capybaraは設定済です。

実行するテストケースを限定したいとき

itの代わりにfitを使います。
spec_helper.rb config.filter_run_when_matching :focusと設定しているため、各テストの頭文字にfをつけることで、focus: trueを設定できます。

別タブで開いたページをテストしたいとき

リンクをクリックし、遷移したページ先の項目についてテストしたいときは、within_window(windows.last)で最後に開いたタブを指定します。

it 'Project詳細からTask一覧ページにアクセスした場合、Taskが表示されること' do
        visit project_path(project)
        click_link 'View Todos' #これでページ遷移する。
        within_window(windows.last)do #以下、遷移先の内容
          expect(page).to have_content task.title
          expect(Task.count).to eq 1
          expect(current_path).to eq project_tasks_path(project)
        end
      end

withinメソッド

Capybaraのwithinメソッドは、ページ内の特定のエリアやアクションを指定できます。
within_windowはウィンドウ画面の指定。

確認画面のページ操作したいとき

何かの項目を削除するとき、「本当に削除しますか?」とアラートメッセージがでますが、それの操作は以下のようにします。

 #許可するとき
page.driver.browser.switch_to.alert.accept 

 #拒否するとき
page.driver.browser.switch_to.alert.dismiss

ApplicationHelperで定義したメソッドをRSpecで使いたいとき

何も考えず使おうとしたら、使えませんでした。私はこういうところをよく忘れるので注意します。
当たり前ですが、includeで読み込む必要があります。
今回は、systemspecファイルでrequire 'rails_helper'しているので、spec/rails_helper.rb内でapplication_helper.rbを読み込む設定をします。

config.include ApplicationHelper #追加

これで、ApplicationHelperで定義したメソッドshort_timeRspecでも使用できます。

it 'Taskを編集した場合、一覧画面で編集後の内容が表示されること' do
        fill_in 'Deadline', with: Time.current #今の時刻にdeadlineを更新する。
        click_button 'Update Task'
        click_link 'Back'
        expect(find('.task_list')).to have_content(short_time(task.reload.deadline)) #ここ
        expect(current_path).to eq project_tasks_path(project)
end

Updateのテストがうまくいかない。値が更新されないとき

上のコードの解説です。
最初、short_time(task.deadline)としたら、更新前の日時しか取得できなかった。更新出来てないってこと?
謎だったけど、その場しのぎでTime.currentにしてしまいました。
そしてちゃんと調べたら、ドンピシャな記事がありました。
DBの値が更新されても、インスタンスの値は更新されない。task.reload.deadlineとする。
これでテストが通りました。

rspecでupdateのテストを通せない初心者が疑うべきこと - Qiita

findメソッド

Capybaraのfindメソッドは特定の要素を指定できます。
さきのコード例では、find('.task_list')で、viewで定義しているclass = "task_list"を指定しています。

strftimeメソッド

Rubyのメソッドです。

時刻を format 文字列に従って文字列に変換した結果を返します。
Time#strftime (Ruby 2.7.0 リファレンスマニュアル)

以下のように使います。

expect(page).to have_content(Time.current.strftime('%Y-%m-%d'))

FactoryBotのtraitを使う

属性をいろいろ指定して定義したいとき、traitを使います。
テスト内で何度も同じ属性を指定して呼び出していると、コードの重複が生じてしまいます。
traitを使えばスッキリとした見た目になります。
また、添付ファイルを予めセットすることもできます。(やり方は『everyday Rails』P174 )

#trait定義前
let!(:task) {create(:task, project_id: project.id, status: :done, completion_date: Time.current.yesterday)}

#trait定義後
let!(:task) { create(:task, :done) }
FactoryBot.define do
  factory :task do
    sequence(:title, "title_1")
    status { rand(2) }
    from = Date.parse("2019/08/01")
    to   = Date.parse("2019/12/31")
    deadline { Random.rand(from..to) }
    association :project

    trait :done do #ここ
      status { :done }
      completion_date { Time.current.yesterday }
    end
  end
end

Capybaraのメソッドについて

公式は例もあって分かりやすい

GitHub - teamcapybara/capybara: Acceptance test framework for web applications

【RSpec】System Spec

やりたいこと

タスクとユーザーのCRUD機能について、テストケースを作成したい。
詰まった部分だけ復習していきます。

前提

  • RSpecのセットアップ済  
  • FactoryBot導入済
  • CRUD機能自体は実装済
  • ログイン認証はsorceryを使用

実装の流れ

  • 必要なgemの導入
  • SystemSpecファイルの作成
  • ドライバの設定
  • モジュールの設定
  • SystemSpecファイルの設定

Gem導入

必要なgemを導入

  gem 'webdrivers' #ブラウザの挙動を確認
  gem 'capybara' #E2Eテスト用フレームワーク
require 'capybara/rspec'

SystemSpecファイルの作成

% rails g rspec:system tasks

※必要に応じて、usersuser_sessionsも作成。

ドライバの設定

ドライバとは、Capybaraを使ったテスト/Specにおいて、ブラウザ相当の機能を利用するために必要なプログラムです。
『現場で使える Ruby on Rails5 速習実践ガイド』P193

RSpec.configure do |config|
  config.before(:each, type: :system)do
    driven_by(:selenium_chrome_headless) #ドライバを一元指定
  end

モジュールの設定

今回、ログイン前・ログイン後に分けてテストを実施します。
モジュールにログイン処理を設定しておきます。
そうすれば、各Specで呼び込むことができるので、コードがすっきりします。
どこにファイルを置くか迷ったのですが、supportディレクトリ内にします。

module LoginModule
  def login(user)
    visit login_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password'
    click_button 'Login'
  end
end

モジュールを作っただけでは使用できないので、includeする必要があります。

  config.include FactoryBot::Syntax::Methods 
  config.include LoginModule #追記

そしてrails_helper.rbを読み込むために、各Specファイルには、上部にrequire 'rails_helper'があります。

require 'rails_helper'

モジュールを読みこめない…

以上のように設定してから、SystemSpecファイル内でloginメソッドを呼び出そうとしたのですが、読み込めませんでした… includeもOK requireもOKなのに、なぜ・・・?

An error occurred while loading ./spec/system/users_spec.rb.
Failure/Error: config.include LoginModule

パスを通してなかった。spec/support以下を読み込むように設定する。
以下のコメントアウトを外して有効化します。

# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } #コメントアウトを外す

タグの設定

タグの機能を使うと、タグを設定したテストだけ実行してくれます。
:focusタグを付けたテストが何もないときは、全てのテストを実行します。

# This allows you to limit a spec run to individual examples or groups
  # you care about by tagging them with `:focus` metadata. When nothing
  # is tagged with `:focus`, all examples get run. RSpec also provides
  # aliases for `it`, `describe`, and `context` that include `:focus`
  # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
  config.filter_run_when_matching :focus #コメントアウトを外す。

SystemSpecファイルの設定

全部書いてると大量になってしまうので、詰まった部分だけ見直していきます。

require 'rails_helper'

RSpec.describe "Users", type: :system do
  let(:user) { create(:user) }
  let(:other_user) { create(:user) }

  describe 'ログイン前' do
    describe 'ユーザー新規登録' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの新規作成が成功する' 
      end

      context 'メールアドレスが未入力' do
        it 'ユーザーの新規作成が失敗する' 
      end

      context '登録済のメールアドレスを使用' do
        it 'ユーザーの新規作成が失敗する' do
          existed_user = create(:user)
          visit sign_up_path
          fill_in 'Email', with: existed_user.email
          fill_in 'Password', with: 'password'
          fill_in 'Password confirmation', with: 'password'
          click_button 'SignUp'
          expect(page).to have_content '1 error prohibited this user from being saved'
          expect(current_path).to eq users_path
          expect(page).to have_content("Email has already been taken")
          expect(page).to have_field 'Email', with: existed_user.email #登録を受け付けなくても、入力したメールアドレスが残ってる。
        end
      end
    end
  end

  describe 'ログイン後' do
    before { login(user) } #設定したモジュールのloginメソッドを呼び出す

      describe 'ユーザー編集' do
        context 'フォームの入力値が正常' do
        it 'ユーザーの編集が成功する' 
      end
        context 'メールアドレスが未入力' do
          it 'ユーザーの編集が失敗する' 
        end

        context '登録済みのメールアドレスを使用' do
          it 'ユーザーの編集が失敗する' do
            visit edit_user_path(user)
            fill_in 'Email', with: other_user.email
            fill_in 'Password', with: 'password'
            fill_in 'Password confirmation', with: 'password'
            click_button 'Update'
            expect(page).to have_content('1 error prohibited this user from being saved')
            expect(page).to have_content("Email has already been taken")
            expect(current_path).to eq user_path(user)
          end
        end
        
        context '他ユーザーの編集ページにアクセス' do
          it '編集ページへのアクセスが失敗する' do
            visit edit_user_path(other_user) #userでログインしている。other_userの編集ページへアクセスする
            expect(current_path).to eq user_path(user)
            expect(page).to have_content("Forbidden access.")
          end
        end
      end

      describe 'マイページ' do
        context 'タスクを作成' do
          it '新規作成したタスクが表示される' do
            create(:task, title:'test', status: :doing, user: user)
            visit user_path(user)
            expect(page).to have_content('You have 1 task.')
            expect(page).to have_content('test')
            expect(page).to have_content('doing')
            expect(page).to have_link('Show')
            expect(page).to have_link('Edit')
            expect(page).to have_link('Destroy')
          end
        end
      end
    end
end
require 'rails_helper'

RSpec.describe "Tasks", type: :system do
  let(:user) { create(:user) } #taskとuserは紐付いているので、両方作成する。
  let(:task) { create(:task) }

  describe 'ログイン前' do
    describe 'ページの遷移確認' do
      context 'タスクの新規登録ページにアクセス' do
        it '新規登録ページのアクセス失敗' 
      end
      context 'タスクの編集ページにアクセス'
        it '編集ページのアクセス失敗' 
      end

      context 'タスクの一覧ページにアクセス' do
        it '全てのユーザーのタスク情報が表示される' do
          task_list = create_list(:task, 3)
          visit tasks_path
          expect(page).to have_content task_list[0].title
          expect(page).to have_content task_list[1].title
          expect(page).to have_content task_list[2].title
          expect(current_path).to eq tasks_path
        end
      end

      context 'タスクの詳細ページにアクセス' do
        it 'タスク詳細ページを見られること' do
          visit task_path(task)
          expect(current_path).to eq task_path(task)
          expect(page).to have_content task.title
        end
      end
    end

  describe 'ログイン後' do
    before { login(user) }

    describe 'タスク新規登録' do
      context 'フォームの入力値が正常' do
        it 'タスクの新規作成が成功する' do
          visit new_task_path
          fill_in 'Title', with: 'test_title'
          fill_in 'Content', with: 'test_content'
          select 'doing', from: 'Status'
          fill_in 'Deadline', with: DateTime.new(2020, 9, 28, 10, 30)
          click_button 'Create Task'
          expect(page).to have_content 'Title: test_title' #作成した内容が表示される
          expect(page).to have_content 'Content: test_content'
          expect(page).to have_content 'Status: doing'
          expect(page).to have_content 'Deadline: 2020/9/28 10:30'
          expect(current_path).to eq '/tasks/1'
        end
      end

      context 'タイトルが空白' do
        it 'タスクの新規登録が失敗すること' 
      end

      context '登録済のタイトルを入力' do
        it 'タスクの新規作成が失敗する' do
          visit new_task_path
          other_task = create(:task)
          fill_in 'Title', with: other_task.title
          fill_in 'Content', with: 'test_content'
          click_button 'Create Task'
          expect(page).to have_content '1 error prohibited this task from being saved'
          expect(page).to have_content 'Title has already been taken'
          expect(current_path).to eq tasks_path
        end
      end
    end

    describe 'タスク編集' do
      let!(:task) { create(:task, user: user) } #letの遅延読み込みではなく、各テストが読み込まれる前に作成する。
      let!(:other_task) { create(:task, user: user) }
      before { visit edit_task_path(task) } #編集ページに遷移しておく。

      context 'フォームの入力値が正常' do
        it 'タスクの更新に成功すること' do
          fill_in 'Title', with: 'title_test'
          select :done, from: 'Status'
          click_button "Update Task"
          expect(current_path).to eq task_path(task)
          expect(page).to have_content("Task was successfully updated")
        end
      end
    end

    describe 'タスク削除' do
      let!(:task) { create(:task, user: user) }

      it 'タスクの削除ができること' do
        visit tasks_path
        click_link "Destroy"
        expect(page.accept_confirm).to eq 'Are you sure?'
        expect(current_path).to eq tasks_path
        expect(page).to have_content("Task was successfully destroyed")
        expect(page).not_to have_content task.title #削除したタスクが表示されてないことを確認
      end
    end
  end
end

create_listで連続するテストデータを作成する。

FactoryBot.create_list(:task,3)

FactoryBotを省略できる設定をしてるのでこのように書ける。
task_list = create_list(:task, 3)

letとlet!の違い

letは遅延読み込みなので、userやtaskが必要になったときだけ作成します。
一方let!は、各テストのブロックが実行される前に作成します。
今回、編集ページのテストを行うには、予めタスクが必要なため、let!で作成しておきます。

let!(:task) { create(:task, user: user) } 
let!(:other_task) { create(:task, user: user) }
before { visit edit_task_path(task) } 

エラーが起こったところ

describe 'マイページ' do
      context 'ログインしていない状態' do
        it 'マイページへのアクセスが失敗する' do
          visit user_path
          expect(current_path).to eq login_path
        end
      end
    end
1) Users ログイン前 マイページ ログインしていない状態 マイページへのアクセスが失敗する
     Failure/Error: visit user_path
     
     ActionController::UrlGenerationError:
       No route matches {:action=>"show", :controller=>"users"}, missing required keys: [:id]

[:id]がないよって怒られる。
user_path(user)が抜けてた。
(user)忘れがちなので注意する。

Rspecの実行

% bundle exec rspec spec/system/tasks_spec.rb

RSpecの結果出力を見易く表示する。

以下のオプションを追記する。

--require spec_helper
--format documentation #追記

参考

『現場で使える Ruby on Rails5 速習実践ガイド』P184
『everyday Rails

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

【Rails】はじめてのSystemSpec(RSpec) - Qiita

【RSpec】モデルスペック

前提

  • RSpecのセットアップ済  

  • FactoryBot導入済

実現したいこと

既存のTaskモデルのバリデーションをチェックしたい

文法に馴染みがなく、簡単なテストなはずなのに半日くらいかかってしまいました。
分かりにくかった部分だけ、復習していきます。

モデルスペックを作成する

% rails g rspec:model task

FactoryBotの設定

  • UserとTaskは1対多の関係です。なので、どちらもテスト用データを作成します。

  • emailtitleはユニーク制約をつけています。

FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "tester#{n}@example.com" }
    password { 'password' }
    password_confirmation { 'password' }
  end
end

一意である必要があるemailについては、sequenceメソッドを使います。
この構文で、ユーザーを生成する度にnの数字を変えて一意のアドレスを作成できます。

FactoryBot.define do
  factory :task do
    sequence(:title, "title_1")
    content { 'Content' }
    status { 'todo' } #enumで定数を設定している。
    deadline { Date.current.tomorrow }
    association :user
  end
end

先程と同様、titleはユニーク制約をつけているので、sequenceメソッドを使用します。
最初は、sequence(:title) { |n| "Title #{n}" }のようにしていたんですが、第二引数を渡すと.nextを呼んでくれて、連番をつけてくれるようです。便利。

String#next (Ruby 2.7.0 リファレンスマニュアル)
FactoryBot (旧FactoryGirl) の sequence と .next - Qiita

deadline { 1.week.from_now } こっちのほうがいいかも
こんな機能があるのは知らなかった。。
Active Support コア拡張機能 - Railsガイド

association :user
これで、TaskとUserに関連があることを設定できる。便利。

FactoryBot省略設定

FactoryBotは通常、FactoryBot.create(略)のようにテストデータを生成できますが、以下の設定をしておくと、FactoryBotを省略できます。

RSpec.configure do |config|
(略)
  config.include FactoryBot::Syntax::Methods #追記
end

Task Model Spec

テストデータ生成の準備が整ったので、Specのコードを書いていきます。
とりあえず今回は、以下の3つに絞ります。

  • 全部の属性を登録したときvalid
  • titleがないときにinvalid
  • titleが重複したときinvalid
require 'rails_helper'

RSpec.describe Task, type: :model do
  describe 'validation' do
    it "is valid with all attributes" do
      task = build(:task) #先の設定のおかげで、FactoryBot.build(:task)と書かなくてもOK
      expect(task).to be_valid
      expect(task.errors).to be_empty #エラーメッセージなし。
    end

    it "is invalid without a title" do
      task_without_title = build(:task, title: nil) #titleはなるべくわかりやすくする。
      expect(task_without_title).to be_invalid
      expect(task_without_title.errors[:title]).to eq ["can't be blank"]
    end

    it "is invalid with a duplicate title" do
      task = create(:task) #1つめのtaskはcreateで保存までする。
      task_with_duplicated_title = build(:task, title: task.title) #2つめのtaskは1つめと同じ名前にする。
      expect(task_with_duplicated_title).to be_invalid
      expect(task_with_duplicated_title.errors[:title]).to eq ["has already been taken"]
    end

全てに共通する流れは、

  • Taskオブジェクトを生成
  • 期待する挙動を検査