パンくずリスト

パンくずリスト

パンくずリストは、以下のようなもの
Image from Gyazo

パンくずリストを作成するメリット
・ユーザーが今どのページにいるのか分かるようにするため
クローラーの巡回を手助けする事でSEO対策になる

クローラー: インターネット上に存在するWEBサイトや画像などの情報を取得し、自動的に検索データベースを作成する巡回プログラム

パンくずリストの導入手順

パンくずリストを表示するには、[gretel]Gemを使う

Gemfileに以下のように記述する

gem "gretel"


$ bundle install


以下コマンドでパンくずリストの設定ファイル([config/breadcrumbs.rb])を作成する
$ $ rails generate gretel:install


[config/breadcrumbs.rb]に下記のようにパンくずリストの設定を記述する

[config/breadcrumbs.rb]

crumb :edit_admin_site do  #  ここには、パンくずの名前を記述
  link '設定', edit_admin_site_path   # [link "ビューに表示される名前", "リンクのURL"]を記述
  parent :admin_dashboard   #  親のパンくずの名前を記述
end

crumb :admin_users do
  link 'ユーザー', admin_users_path
  parent :admin_dashboard
end

crumb :admin_user do |user|  #  リンクのURLにインスタンスを渡さなければいけない時は、このように記述
  link 'プロフィール', admin_user_path(user)
  parent :admin_users
end

crumb :new_admin_user do
  link 'ユーザーの作成', new_admin_user_path
  parent :admin_users
end

crumb :admin_invitations do
  link '招待', admin_invitations_path
  parent :admin_dashboard
end

crumb :new_admin_invitation do
  link '招待状の作成', new_admin_invitation_path
  parent :admin_invitations
end

crumb :admin_categories do
  link 'カテゴリー', admin_categories_path
  parent :admin_dashboard
end

crumb :edit_admin_category do |category|
  link 'カテゴリー編集', edit_admin_category_path(category)
  parent :admin_categories
end

crumb :admin_authors do
  link '著者', admin_authors_path
  parent :admin_dashboard
end

crumb :edit_admin_author do |author|
  link '著者編集', edit_admin_author_path(author)
  parent :admin_authors
end

crumb :admin_articles do
  link '記事', admin_articles_path
  parent :admin_dashboard
end

crumb :new_admin_article do
  link '記事作成', new_admin_article_path
  parent :admin_articles
end

crumb :edit_admin_article do |article|
  link '記事編集', edit_admin_article_path(article.uuid)
  parent :admin_articles
end

crumb :admin_tags do
  link "タグ", admin_tags_path
  parent :admin_dashboard
end

crumb :edit_admin_tag do |tag|
  link "タグ編集", edit_admin_tag_path(tag)
  parent :admin_tags
end


パンくずリストを表示したいビューの部分に以下のように記述

<% breadcrumb パンくずの名前 %>

設定したパンくずリストにリンクでURLにインスタンスを渡さなければいけない時は、以下のように記述する

<% breadcrumb パンくずの名前, 渡すインスタンス %>

参考記事:

クローラーとは?SEOに必要な基礎知識とクローリングを最適化する方法 :: 株式会社イノーバ

【Rails】 gretelを使ってパンくずリストを作成しよう | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

rails コマンドが使えない時の対処法

rails コマンドが使えない時の対処法

下記のようにrailsコマンドが使えない時は、[rails]Gemがインストールされていない可能性がある。

Image from Gyazo

rbenvなどでRubyのバージョンを新しくした場合などに発生することがある

通常は、下記のように[bundle]と[rails]が存在するが、どちらかが存在しなかったり、両方存在しないことがある

Image from Gyazo

対処法

・[rails]が無かった場合は、下記のように今回使用しているRubyのバージョンのファイルに移動して、下記のように実行して[rails]Gemをインストールする

Image from Gyazo

$ gem install rails -v 5.2.5 上記は、インストールしたrailsのバージョンを指定する

・[bundle]Gemが無かった場合は、上記と同じように対象のディレクトリに移動して以下を実行

$ bundle install

・両方ない場合は、

$ bundle install


$ gem install rails

参考記事:

「rbenv: rails: command not found」の対処法 - Qiita

リンク時にタブを変えるHTML記載方法

リンクをクリックした時にタブを変えるHTML記載方法

下記のように、[target:'_blank']をオプションで記述する

<%= link_to 'View Todos', project_tasks_path(@project), target:'_blank', rel: 'noopener' %>

※ポイント

RSpecテスト実行時の注意点 下記のようなRSpecテストで[expect(page).to have_content task.title]のように、指定したコンテントがpage内にあるのを期待しているような記述をする時は、実際のブラウザ上ではタブが変わってしまっているので、希望しているコンテントは新規のタブに表示されるのでRSpecテストがエラーになってしまう
新規タブでRSpecで指定したコンテントを探すためには、使用するドライバの設定を変える必要がある

<%= link_to 'View Todos', project_tasks_path(@project), target:'_blank', rel: 'noopener' %>
      it 'Project詳細からTask一覧ページにアクセスした場合、Taskが表示されること' do
        # FIXME: テストが失敗するので修正してください
        visit project_path(project)
        click_link 'View Todos'
        expect(page).to have_content task.title
        expect(Task.count).to eq 1
        expect(current_path).to eq project_tasks_path(project)
      end
[spec/support/driver_setting.rb]

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by(:rack_test)  #タブが切り替える時に使う
    # driven_by(:selenium_chrome)   #  RSpecでのブラウザの自動操作の動きをブラウザで確認したい時に利用
    # driven_by(:selenium_chrome_headless)  #  RSpecでブラウザを自動操作でテストを実行してブラウザの動きを見ない場合に利用(headless)
  end
end

・リンクの記述で[target:'_blank']オプションを付与する時の注意点
セキュリティの都合上[rel: 'noopener']オプションも必ず付与しなければいけない(ブラウザによっては、[noopener]がサポートされていないので、[norefferer]も指定しておいた方が良い)

参考記事:

aタグはtarget="_blank"だけじゃNG!rel="noopener noreferrer"をつける - こがMemo

実はヤバい?aタグと別タブで開く(target=”_blank”)の使い方 |ホームページ制作 名古屋 愛知 |株式会社WWG ダブルダブルジー

window.opener - Web API | MDN

byebug Gem

[byebug]Gemの導入と使い方

Gemfileに以下のように記述する(railsをインストールしている場合は、デフォルトでインストールされている)

gem 'byebug'


$ bundle install


下記のようにデバッグしたいところに[byebug]を記述する

[spec/system/task_spec.rb]

RSpec.describe 'Task', type: :system do
  let(:project) { create(:project) }

  describe 'Task削除' do
    context '正常系' do
      # FIXME: テストが失敗するので修正してください
      it 'Taskが削除されること' do
        task = FactoryBot.create(:task, title: "test_title", project_id: project.id)
        visit project_tasks_path(project)
        click_link 'Destroy'
        page.driver.browser.switch_to.alert.accept
        byebug
        expect(page).not_to have_content task.title
        expect(Task.count).to eq 0
        expect(current_path).to eq project_tasks_path(project)
      end
    end
  end

上記のように記述して、以下のようにテストを実行すると[byebug]を記述した部分で処理が止まり、下記のように表示される

$ bundle exec rspec

Image from Gyazo

例えば、[next]を入力して実行すると次の行に移動する

※ポイント

・下記のように[help]を入力して実行すると、[byebug]で処理が止まっているところで使えるコマンドが確認できる

Image from Gyazo

・下記のように記述して実行するファイルを指定することもできる
$ byebug 実行するファイル名

・下記のようなコードがあり、[byebug]で処理を止めたところで[page.body]を実行するとRSpec上でのブラウザの表示画面のHTMLが確認できる

  describe 'Task削除' do
    context '正常系' do
      # FIXME: テストが失敗するので修正してください
      it 'Taskが削除されること' do
        task = FactoryBot.create(:task, title: "test_title", project_id: project.id)
        visit project_tasks_path(project)
        click_link 'Destroy'
        page.driver.browser.switch_to.alert.accept
        byebug
        expect(page).not_to have_content task.title
        expect(Task.count).to eq 0
        expect(current_path).to eq project_tasks_path(project)
      end
    end
  end

下記が[page.body]を実行して得られたRSpecのブラウザ上のHTML

Image from Gyazo

[/n]は、改行を示している

・[byebug]で処理を止めている箇所で変数の確認をすることもできる

参考記事:

byebugの導入から使い方まで - Qiita

Ruby / Ruby on Rails デバッグ方法まとめ - Qiita

byebugでRubyスクリプトをコマンドラインデバッグする - Qiita

RSpecのシステムテスト

RSpecシステムテストの手順と記述

Gemfileに以下のように記載する

[Gemfile]

group :test do
  gem "capybara"
  gem 'webdrivers'
end

※ ・[capybara]Gem: ブラウザ上でのリンクのクリック、フォームの入力等のUI操作コマンドで実行できるようにするもの。
・[webdrivers]Gem: ChromeDriverを簡単に導入してくれるGem(プログラミングを介して、ブラウザを操作するライブラリ)

・ChromeDriver: Google Chromeを操作するために必要なドライバ(ソフト)

・WebDriver: ブラウザの操作自体をブラウザの拡張機能やOSの機能を利用して、実行できるようなツール

Selenium: ブラウザ操作を自動化するツール

・headlessブラウザ: 画面に表示されないブラウザを裏で実行する


$ bundle install


[.rspec]ファイルに以下のように記述

[.rspec ファイル]

--require spec_helper
--format documentation

[--format documentation]を加えることでテスト実行時の結果がメッセージ付きで見やすくなる


[spec/support]以下のファイルを読み込ませたり、作成したmoduleを読み込ませるために以下のようにコメントアウトを外したり記述を追加する

[spec/rails_helper.rb]

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }  # [spec/support]以下の全てのファイルが自動的に読み込まれる①

config.include LoginModule  #  自分で作成したmoduleを読み込む(こちらはファイルの1番下に追加する )

spec配下にある[spec.rb]で終わるファイルは[bundle exec rspec]を実行すると自動的に読み込まれるので、このファイルを[spec/support]以下に置くと読み込み時とテスト時で2回実行されてしまうので、[spec/support]以下に[spec.rb]で終わるファイルは、置かない。①の記述をすることで、[spec/support]以下の全てのファイルが自動的に読み込まれる


下記は、SystemSpecを実行するドライバの設定をしている
ドライバとは、Capybaraを使ったテストにおいてブラウザの代わりになるもの(今回は、処理が高速なHeadlessChormeを利用)

[spec/support/capybara.rb]

RSpec.configure do |config|
  config.before(:each, type: :system) do  #  System Specを使用するための設定
    driven_by :selenium, using: :headless_chrome  # Spec実行時にブラウザを起動せずにテストを実施できる(Specテストでブラウザに相当するプログラムを指定している。ここでは、ブラウザを画面に表示しないHeadlessブラウザの1種headless_chromeを使っている。)
  end
end


[spec/spec_helper.rb]を以下のように記述(=beginの位置を変える)する事で、[config.filter_run_when_matching :focus]をオンにすることができる。
[config.filter_run_when_matching :focus]をオンにすると、限定的にテストを実行することができる

[spec/spec_helper.rb]

# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.

  # 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
  
=begin


以下のように[focus: true]オプションを付与するとテストを限定的に実行できるので、実行したいテストのみを実行でき無駄な時間がかからない

  describe 'ログイン前' do
    describe 'ページ遷移確認' do
      context 'タスクの新規登録ページへアクセス' do
        it 'タスクの新規登録ページへのアクセスに失敗する' do
          visit new_task_path
          expect(page).to have_content "Login required"
          expect(current_path).to eq login_path
        end
      end

      context 'タスクの編集ページへのアクセス', focus: true do  # ここの部分
        it 'タスクの編集ページへのアクセスに失敗する' do
          visit edit_task_path(task)
          expect(page).to have_content "Login required"
          expect(current_path).to eq login_path
        end
      end


rspec実行時にログイン処理が必要になる項目があるので、moduleでログイン処理をまとめておく(このmoduleは、[spec/rails_helper.rb]で読み込んでおり、各rspecテストは、require記述により、[spec/rails_helper.rb]を読み込んでいるので使える)

[spec/support/login_module.rb]

module LoginModule
    def login_as(user)
        visit login_path
        fill_in "email", with: user.email
        fill_in "password", with: "password"
        click_button "Login"
    end
end

上記記述で、[fill_in "password", with: user.crypted_password]のように記述しても[sorcery]Gemによってパスワードがハッシュ化されており、ハッシュ化されたパスワードからは、元々のパスワードが割り出せないため、[FactoryBot]Gemでパスワードは、[password]で固定(userが何度生成されても同じ)されているため、直接[password]をfill inに入力している


下記がRSpecのsystemテストになる

[spec/system/tasks_spec.rb]

require 'rails_helper'

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

  describe 'ログイン前' do
    describe 'ページ遷移確認' do
      context 'タスクの新規登録ページへアクセス' do
        it 'タスクの新規登録ページへのアクセスに失敗する' do
          visit new_task_path
          expect(page).to have_content "Login required"
          expect(current_path).to eq login_path
        end
      end

      context 'タスクの編集ページへのアクセス' do
        it 'タスクの編集ページへのアクセスに失敗する' do
          visit edit_task_path(task)
          expect(page).to have_content "Login required"
          expect(current_path).to eq login_path
        end
      end

      context 'タスクの詳細ページへのアクセス' do
        it 'タスクの詳細ページへアクセスされる' do
          visit task_path(task)
          expect(page).to have_content task.title
          expect(page).to have_content task.content
          expect(page).to have_content task.status
          expect(current_path).to eq task_path(task)
        end
      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[0].content
          expect(page).to have_content task_list[0].status
          expect(page).to have_content task_list[1].title
          expect(page).to have_content task_list[1].content
          expect(page).to have_content task_list[1].status
          expect(page).to have_content task_list[2].title
          expect(page).to have_content task_list[2].content
          expect(page).to have_content task_list[2].status
          expect(current_path).to eq tasks_path
        end
      end
    end
  end

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

    describe 'タスクの新規登録' do
      context 'フォームの入力値が正常' do
        it 'タスクの新規登録が成功する' do
          visit root_path
          click_link "New task"
          fill_in "task_title", with: "test"
          fill_in "task_content", with: "test_content"
          find("#task_status").find("option[value = 'todo']").select_option   ①
          fill_in "task_deadline", with: DateTime.new(2021, 8, 1, 10, 30)
          click_button "Create Task"
          expect(page).to have_content "Task was successfully created."
          expect(page).to have_content "test"
          expect(page).to have_content "test_content"
          expect(page).to have_content "todo"
          expect(current_path).to eq '/tasks/1'end
      end

      context 'タイトルが未入力' do
        it 'タスクの新規登録が失敗する' do
          visit root_path
          click_link "New task"
          fill_in "task_title", with: ""
          fill_in "task_content", with: "test_content"
          find("#task_status").find("option[value='todo']").select_option
          fill_in "task_deadline", with: DateTime.new(2021, 8, 1, 10, 30)
          click_button "Create Task"
          expect(page).to have_content "1 error prohibited this task from being saved:"
          expect(page).to have_content "Title can't be blank"
          expect(current_path).to eq tasks_path
        end
      end

      context '登録済みのタイトルを入力' do
        it 'タスクの新規登録が失敗する' do
          task = create(:task)
          visit root_path
          click_link "New task"
          fill_in "task_title", with: task.title
          fill_in "task_content", with: "test_content"
          find("#task_status").find("option[value='todo']").select_option
          fill_in "task_deadline", with: DateTime.new(2021, 8, 1, 10, 30)
          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(:other_task) { create(:task, user: user) }

      context 'フォームの入力値が正常' do
        it 'タスクの編集に成功する' do
          visit root_path
          click_link "Edit"
          fill_in "task_title", with: "update_title"
          fill_in "task_content", with: "update_content"
          find("#task_status").find("option[value='doing']").select_option
          click_button "Update Task"
          expect(page).to have_content "Task was successfully updated."
          expect(page).to have_content "update_title"
          expect(page).to have_content "update_content"
          expect(page).to have_content "doing"
          expect(current_path).to eq task_path(task)
        end
      end

      context 'タイトルが未入力' do
        it 'タスクの編集に失敗する' do
          visit root_path
          click_link "Edit"
          fill_in "task_title", with: ""
          fill_in "task_content", with: task.content
          find("#task_status").find("option[value='todo']").select_option
          click_button "Update Task"
          expect(page).to have_content "1 error prohibited this task from being saved:"
          expect(page).to have_content "Title can't be blank"
          expect(current_path).to eq task_path(task)
        end
      end

      context '登録済みのタイトルを入力' do
        it 'タスクの編集に失敗する' do
          visit root_path
          click_link "Edit"
          fill_in "task_title", with: other_task.title
          fill_in "task_content", with: "test_content"
          find("#task_status").find("option[value='todo']").select_option
          click_button "Update 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 task_path(task)
        end
      end
    end

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

      it 'タスクの削除に成功する' do
        visit root_path
        click_link "Destroy"
        expect(page.accept_confirm).to have_content "Are you sure?"
        expect(page).to have_content "Task was successfully destroyed."
        expect(current_path).to eq tasks_path
        expect(page).not_to have_content task.title
      end
    end
  end
end
[spec/system/user_sessions_spec.rb]

require 'rails_helper'

RSpec.describe "UserSessions", type: :system do
  let(:user) { create(:user) }
  
  describe 'ログイン前' do
    context 'フォームの入力値が正常' do
      it 'ログイン処理が成功する' do
        visit login_path
        fill_in "email", with: user.email
        fill_in "password", with: "password"
        click_button "Login"
        expect(page).to have_content "Login successful"
        expect(current_path).to eq root_path
      end
    end
    
    context 'フォームが未入力' do
      it 'ログイン処理が失敗する' do
        visit login_path
        fill_in "email", with: ""
        fill_in "password", with: ""
        click_button "Login"
        expect(page).to have_content "Login failed"
        expect(current_path).to eq login_path
      end
    end
  end

  despecscribe 'ログイン後' do
    context 'ログアウトボタンをクリック' do
      it 'ログアウト処理が成功する' do
        login_as(user)
        visit root_path
        click_link "Logout"
        expect(page).to have_content "Logged out"
        expect(current_path).to eq root_path
      end
    end
  end

end
[spec/system/users_spec.rb]

require 'rails_helper'

RSpec.describe "Users", type: :system do
  let(:user) { create(:user)}
  
  describe 'ログイン前' do
    describe 'ユーザー新規登録' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの新規作成が成功する' do
          visit root_path
          click_link "SignUp"
          fill_in "user_email", with: 'sample@sample.com'
          fill_in "user_password", with: 'password'
          fill_in "user_password_confirmation", with: 'password'
          click_button "SignUp"
          expect(page).to have_content "User was successfully created."
          expect(current_path).to eq login_path
        end
      end

      context 'メールアドレスが未入力' do
        it 'ユーザーの新規作成が失敗する' do
          visit root_path
          click_link "SignUp"
          fill_in "user_email", with: ""
          fill_in "user_password", with: 'password'
          fill_in "user_password_confirmation", with: 'password'
          click_button "SignUp"
          expect(page).to have_content "1 error prohibited this user from being saved:"
          expect(page).to have_content "Email can't be blank"
          expect(current_path).to eq users_path
        end
      end

      context '登録済のメールアドレスを使用' do
        it 'ユーザーの新規作成が失敗する' do
          other_user = create(:user)
          visit root_path
          click_link "SignUp"
          fill_in "user_email", with: other_user.email
          fill_in "user_password", with: "password"
          fill_in "user_password_confirmation", with: "password"
          click_button "SignUp"
          expect(page).to have_content "1 error prohibited this user from being saved:"
          expect(page).to have_content "Email has already been taken"
          expect(page).to  have_field "user_email", with: other_user.email
          expect(current_path).to eq users_path
        end
      end
    end
 
    describe 'マイページ' do
      context 'ログインしていない状態' do
        it 'マイページへのアクセスが失敗する' do
          visit user_path(user)
          expect(page).to have_content "Login required"
          expect(current_path).to eq login_path
        end
      end
    end
  end
 
  describe 'ログイン後' do
    before { login_as(user) }

    describe 'ユーザー編集' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの編集が成功する' do
          visit users_path
          click_link "Edit"
          fill_in "user_email", with: "test@sample.com"
          fill_in "user_password", with: "test"
          fill_in "user_password_confirmation", with: "test"
          click_button "Update"
          expect(page).to have_content "User was successfully updated."
          expect(current_path).to eq user_path(user)
        end
      end

      context 'メールアドレスが未入力' do
        it 'ユーザーの編集が失敗する' do
          visit users_path
          click_link "Edit"
          fill_in "user_email", with: ""
          fill_in "user_password", with: "password"
          fill_in "user_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 can't be blank"
          expect(current_path).to eq user_path(user)
        end
      end

      context '登録済のメールアドレスを使用' do
        it 'ユーザーの編集が失敗する' do
          other_user = create(:user)
          visit users_path
          click_link "Edit"
          fill_in "user_email", with: other_user.email
          fill_in "user_password", with: "password"
          fill_in "user_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
          other_user = create(:user)
          visit edit_user_path(other_user)
          expect(page).to have_content "Forbidden access."
          expect(current_path).to eq user_path(user)
        end
      end
    end
 
    describe 'マイページ' do
      context 'タスクを作成' do
        it '新規作成したタスクが表示される' do
          task = create(:task, user: user)
          visit user_path(user)
          click_link "Task list"
          expect(page).to have_content task.title
          expect(page).to have_content task.content
          expect(page).to have_content task.status
          expect(page).to have_link "Show"
          expect(page).to have_link "Edit"
          expect(page).to have_link "Destroy"
          expect(current_path).to eq tasks_path
        end
      end
    end
  end
end

下記のようなセレクトボタンは、①のような表記ではなく[select 'doing', from: 'Status']のように記述することができる

Image from Gyazo

②の部分を[expect(current_path).to eq task_pth(task)]のように記述すると、let(:task) { create(:task) }が呼ばれてletで生成されたtaskの詳細が表示されるので、今ここで作成したタスクの詳細が表示されない
[expect(current_path).to eq '/tasks/1']のように記述するのは、1つ目に生成されたtaskだからこのように記述できる

※ポイント
RSpecの[it]を[xit]とするとpend扱いになり、他の箇所だけをテスト実行できる

RSpecの[it]を[fit]とするとfocusしてピンポイントでテストを実行できる

・[fill_in]の使い方
[fill_in "フォームのid値 or フォームのラベルの名前", with "フォームに入力する内容"]
フォームのラベルの名前でうなくいかない時は、フォームのid値で指定する

・確認ダイアログのrspec
Image from Gyazo

上記のような確認ダイアログのrspecでの確認テストは、以下のようになる

      it 'タスクの削除に成功する' do
        click_link "Destroy"
        expect(page.accept_confirm).to have_content "Are you sure?"   #  確認ダイアログに表示されているメッセージを確認して、OKをクリック
        expect(page).to have_content "Task was successfully destroyed."   #  確認ダイアログでOKをクリックした後の画面に表示されているメッセージを確認
        expect(current_path).to eq tasks_path
        expect(page).not_to have_content task.title
      end

[accept_confirm]メソッド: Capybaraのメソッドで確認ダイアログのOKボタンを押す
[dismiss_confirm]メソッド: Capybaraのメソッドで確認ダイアログのキャンセルボタンを押す

確認ダイアログをクリックするRSpecテストは、下記のように記述することもできる

page.driver.browser.switch_to.alert.accept    #    OKボタンをクリック

page.driver.browser.switch_to.alert.dismiss   #   キャンセルボタンをクリック

・[have_field]マッチャの使い方
[expect(page).to have_field "フォームのid値 or フォームのラベルの名前", with: "入力されている内容"]

ページ内の指定したフォームにwith以下の内容がフォームに入っているかを確認するマッチャ

・更新に失敗した時のURL
下記のように更新に失敗した時のURLは、updateアクションのURLになるので、rspecで更新に失敗した時点でのパスを確認するテストを記述する時は、画面に連られてeditアクションのパスを記載しないように注意する(更新に失敗した時は、編集画面をrenderしているだけであり、ルーティング処理を通らずupdateで失敗した時点で止まったままなので、updateのパスになっている)

Image from Gyazo

・letの扱い
letは、letで定義している変数が呼ばれる度に新しくインスタンスを生成すると思っていたが、同じexample内(同じit内)で再度letを呼ぶ時は、最初に呼ばれた値をキャッシュして同じインスタンスを使い回すようになる

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

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

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

      it 'タスクの削除に成功する' do
        visit root_path
        click_link "Destroy"
        expect(page.accept_confirm).to have_content "Are you sure?"
        expect(page).to have_content "Task was successfully destroyed."
        expect(current_path).to eq tasks_path
        expect(page).not_to have_content task.title
      end
    end

上記のような場合に①で生成されたuserと②で使われているuserは、内容が同じuserになる

・下記のような[date_field]で作成したフォームのrspecテストは、以下のように記載できる。

Image from Gyazo

fill_in "task_deadline", with: DateTime.new(2021, 8, 1, 10, 30)

上記の他にも下記のような記述方法がある

fill_in 'task_deadline', with: "00/2020/06/01/10:30"
fill_in 'Deadline', with: '002020-06-01-10:30'

基本的には、[DateTime.new]で記載するのが王道

RSpecの日時表記の注意
RSpecで日時の表記などをテスト項目にする際は、以下のようにRSpecの日時表記をビューに合わせなければいけない

[app/views ファイル]

task.deadline.strftime("%-m/%d %-H:%M")
[spec/system/task_spec.rb]

      it '既にステータスが完了のタスクのステータスを変更した場合、Taskの完了日が更新されないこと' do
        # TODO: FactoryBotのtraitを利用してください
        task = FactoryBot.create(:task, project_id: project.id, status: :done, completion_date: Time.current.yesterday)
        visit edit_project_task_path(project, task)
        select 'todo', from: 'Status'
        click_button 'Update Task'
        expect(page).to have_content('todo')
        expect(page).not_to have_content(Time.current.strftime('%Y-%m-%d'))
        expect(current_path).to eq project_task_path(project, task)
      end

[strftime]メソッドは、時刻を指定した文字列に従って文字列に変換し結果を返す

Time#strftime (Ruby 3.0.0 リファレンスマニュアル)

参考記事:

【Rails】『RSpec + FactoryBot + Capybara + Webdrivers』の導入&初期設定からテストの書き方まで | vdeep

Selenium WebDriverでRubyのテストを行う方法【初心者向け】 | TechAcademyマガジン

【RSpec】spec/rails_helper.rbを和訳&補足してみた - Qiita

【Rails】Rspecでテストコードを書く【78日目】|かわいかわ|note

【Rspec】Capybaraについて | プログラミングマガジン

RSpec letとlet!とbeforeの実行されるタイミング.|いずは. フォロバ100%します.毎朝7時.|note

rspec-rails 3.7の新機能!System Specを使ってみた - Qiita

特定のテストケースを実行したい時のfocus: true - その辺にいるWebエンジニアの備忘録

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

【Rails】RSpecとCapybaraのFeatureテスト書き方まとめ | にょけんのボックス

【RSpec/capybara】fill_inが上手く動作しない - Qiita

テストの基本構造 - RSpec/Capybara入門 - Ruby on Rails with OIAX

Capybara Rspec UIテストのめっちゃ基礎

RSpec3でspec/support内のファイルを読み込む - Qiita

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita

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

RSpecコトハジメ ~初期設定マニュアル~ - Qiita

Pythonでブラウザ操作を自動化するSelenium WebDriverの使い方 – Valmore

【Rails】『RSpec + FactoryBot + Capybara + Webdrivers』の導入&初期設定からテストの書き方まで | vdeep

Rspec 複数のselectタグのテストを実行する時の注意 - Qiita

Capybara でデータ削除時の confirm ボタンを押したい - Just do IT

RSpec初心者個人的メモ

[RSpec]ダイアログのテスト方法 – TWEI blog

have_fieldマッチャで指定した入力フィールドを確認する|SportechなエンジニアBlog

let, インスタンス変数, ローカル変数, 定数 in RSpec - Qiita

【Rspec】テストコードの処理の共通化(before、let、shared_examples) | プログラミングマガジン

Railsチュートリアル卒業者に捧げる、RSpec超入門 - 28歳からはじめるフリーランスLIFE!

【RSpec】テストの際、日付・時刻フォーム(datetime_select)に値を入力する - Qiita

【Rspec】System Spec(システムスペック)の基本 | プログラミングマガジン

Headless Chromeを試してみる - Qiita

ブロックチェーンの技術「一方向性ハッシュ関数」を理解しよう

RSpec - SystemSpecでのdate_fieldへの入力方法|teratail

【Rails入門】RSpecを使ったテスト方法を初心者向けに基本から解説 | 侍エンジニアブログ

CapybaraでJSのconfirmダイアログのボタンを押す - Qiita

【Rails】Selenium/RSpecでconfirmダイアログのテストをする - Qiita

<!DOCTYPE html>について

<!DOCTYPE html>について

<!DOCTYPE html>のDOCTYPE宣言についてハマったことがあり、その重要性を知ったので、こちらにまとめます。

今回ハマったことは、以下になります。

[app/views/layouts ファイル]

<html>
  <head>
    <meta charset="utf-8">
    <meta lang='ja'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><%= page_full_title(yield(:title), admin: true) %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag    'admin', media: 'all' %>
    <%= javascript_include_tag 'admin', 'common' %>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
  </head>

  <body class="hold-transition sidebar-mini layout-fixed">
    <div class="wrapper">
      <%= render 'admin/shared/header' %>
      <%= render 'admin/shared/menu' %>
    
      <!-- Content Wrapper. Contains page content -->
      <div class="content-wrapper">
        <%= render 'shared/flash_message' %>
        <%= yield %>
      </div>
      
      <%= render 'admin/shared/footer' %>
    </div>
  </body>
</html>
[app/views/admin/shared/_footer.html.erb]

<footer class="main-footer">
    <strong>Copyright &copy; 2019 RUNTEQ.</strong> All rights reserved.
</footer>

上記のように記述しており、ブラウザを確認すると以下のようにフッターが途中で出てきてしまっている。

Image from Gyazo

[yeild]よりも下でフッターを読み込ませているのに何故だ?とドツボにハマり、全く関係ないところを調べたりしてしまった。

下記のようにlayoutファイルを記述したことで、ちゃんとフッター部分にフッターが配置された。

[app/views/admin/layouts ファイル]

<!DOCTYPE html>    #  これを付け加えた
<html>
  <head>
    <meta charset="utf-8">
    <meta lang='ja'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><%= page_full_title(yield(:title), admin: true) %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag    'admin', media: 'all' %>
    <%= javascript_include_tag 'admin', 'common' %>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
  </head>

  <body class="hold-transition sidebar-mini layout-fixed">
    <div class="wrapper">
      <%= render 'admin/shared/header' %>
      <%= render 'admin/shared/menu' %>
    
      <!-- Content Wrapper. Contains page content -->
      <div class="content-wrapper">
        <%= render 'shared/flash_message' %>
        <%= yield %>
      </div>
      
      <%= render 'admin/shared/footer' %>
    </div>
  </body>
</html>

今回フッターがフッター部分に配置されなかったのは、

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