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]以下の全てのファイルが自動的に読み込まれる①

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

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 'todo', from: 'Status'
や
within 'select[name="q[tag_id]"]' do   # [name="q[tag_id]"]は、selectボタンの名前
  select 'todo'
end

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だからこのように記述できる

※ポイント
・下記のような記述をするとwithinで操作の範囲を設定できる(withinの中にクラスを指定して)

within('.breadcrumb') do
  expect(page).to have_link "Home"
  expect(page).to have_link "タグ"
  expect(page).to have_content "タグ編集"
end

・switch_to_window(windows.last)で、タブが切り替わった場合は、タブを指定して検証できる。(この場合は、[windows.last]で最後のタブを指定している)

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

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

・[be_truthy]の使い方

expectの中に記述したものが、trueであることを期待する場合に使う

# 以下が例
expect(mail.present?).to be_truthy

・[be_falsey]の使い方

expectの中に記述したものが、falseであることを期待する場合に使う

# 以下が例
expect(mail.present?).to be_falsey

・[fill_in]の使い方
[fill_in "フォームのid値 or フォームのラベルの名前", with "フォームに入力する内容"]
フォームのラベルの名前でうなくいかない時は、フォームのid値で指定する
※ポイント
inputタグにlabelやid属性、name属性がない場合は、以下のようにすれば使えるようになる。
ex) 下記のようになっている場合
Image from Gyazo

<div>お名前</div>
<input size="40" placeholder="サンプル太郎" " type="text"">
[spec/system/orders.spec.rb]

find(:xpath, '//div[text()="お名前"]//following-sibling::input').set('テスト太郎')

# xpathは、htmlの構造を把握する感じのもの
# div[text()="お名前"]で、divタグの中の文字列が'お名前'の要素というもの
# following-sibling::inputで、指定した要素(今回の場合は、divタグの中の文字列が'お名前')の次の要素で且つinputタグのものという意味
# setでそのフォームに文字列をセットする

have_selectの使い方

expect(page).to have_select('ラベル名 or フィールドのid or フィールドのclass', selected: '選択されている文言')

have_selectorの使い方

expect(page).to have_selector(:css, '.form-control', text: '下書き')
や
expect(page).to have_selector("input[value=#{author.name}]"),

・確認ダイアログの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: "入力されている内容"]

・[have_http_status]マッチャの使い方

expect(page).to have_http_status(200)     # HTTPのステータスコードをテストできる

・[attach_file]マッチャの使い方(画像のアップロード)

attach_file 'inputタグの画像アップロードのname部分 or labelのforの部分', "アップロードしたい画像のパス"

# 以下が例
attach_file 'article[eye_catch]', "#{Rails.root}/spec/fixtures/images/test.jpeg"

# 複数の画像をアップロードする時
attach_file 'article[eye_catch]', %w(spec/fixtures/images/test.png spec/fixtures/images/test2.png))

上記でアップロードした画像を検証するのは、以下の記述でできる

expect(page).to have_selector("img[src$='画像のファイル名']")

# 以下が例
expect(page).to have_selector("img[src$='test.jpeg']")

・[have_checked_field]マッチャの使い方(ラジオボタンで選択されている状態)

Image from Gyazo

expect(page).to have_checked_field '左寄せ'

・[have_unchecked_field]マッチャの使い方(ラジオボタンで選択されていない状態)

Image from Gyazo

expect(page).to have_unchecked_field '中央'

・[choose]マッチャの使い方(ラジオボタンで指定したものを選択できる)

Image from Gyazo

choose '左寄せ'

・[have_field]マッチャの使い方(フォームに入力されている値を検出できる)

expect(page).to have_field '(ラベル名)', with:(検出する値)

# 以下が例
expect(page).to have_field '横幅', with: 200

・[sleep]マッチャの使い方(ブラウザ上で前の処理が完了するのに時間がかかり、ブラウザ上で処理をしている間にテストコードが進んでしまいテストが失敗するのを防ぐ為にsleepで指定した秒数テストを待つ)

sleep (秒数)

# 以下が例(プレビューボタンをクリックした後にブラウザで表示されるまでに時間がかかる場合は、テストが失敗してしまうので以下の例のように2秒待つようにする)
click_on('プレビュー')
sleep 2
expect(page).to have_selector(".twitter-tweet")

・[click_button]マッチャの使い方(ブラウザ上でのbuttonをクリック(inputタグなど))

click_button('ボタンの表示名 or ボタンのid値')

# 以下が例
click_button('更新する')

・[click_link]マッチャの使い方(ブラウザ上でのlinkをクリック(aタグなど))

click_link('リンクの表示名 or リンクのid値')

# 以下が例
click_link('プレビュー')

・[click_on]マッチャの使い方(リンクかボタンか分からない時にどちらの場合でもクリックしてくれる便利なやつ)

click_on('リンクの表示名 or リンクのid値')

# 以下が例
click_on('プレビュー')

・[click]マッチャの使い方(click_onなどがうまく機能しない時は、以下のようにして使える)

find('id or クラス名').click

#以下が例
find('.search-button'').click

・画面に同じ表示名のボタンがある時に指定したボタンをクリックする方法

# ページ全体でclass名が[box-footer]になっている1つ目の[更新する]という表示名のボタンをクリック
page.all('.box-footer')[0].click_button('更新する')

・要素の属性値を検証する方法

<input placeholder="料理をした日" class="me-1 cooking-memories-search-select-form" type="search" name="q[cooking_date_gteq]" id="q_cooking_date_gteq">
begin_cooking_date_search_form = find('#q_cooking_date_gteq')   ①
expect(begin_cooking_date_search_form['type']).to eq 'date'


①で要素を取得して要素["属性名"]で属性値を取得できる

・ActionMailerのテスト(以下に例を示します)

  let(:mail) { ArticleMailer.report_summary.deliver_now }  # テストファイルでも[(メーラー名).(メソッド名).deliver_now]でメールを送信できる
  let(:check_sent_mail) {
    expect(mail.present?).to be_truthy
    expect(mail.to).to eq(['admin@example.com'])
    expect(mail.subject).to eq('公開済記事の集計結果')
  }

[mail.to]で送信先を検証できる
[mail.from]で送信元を検証できる
[mail.subject]でメールの件名を検証できる
[mail.body]でメールの本文を検証できる

・更新に失敗した時の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.2 リファレンスマニュアル)

rspecでの[transient]と[after(:build)]について

[spec/system/admin_articles_spec.rb]

let(:article_with_tag) { create(:article, :with_tag, tag_name: 'Ruby') }       ①
let(:article_with_another_tag) { create(:article, :with_tag, tag_name: 'PHP') }

    it 'タグで絞り込み検索ができること' do
      article_with_tag    ①
      article_with_another_tag
      visit admin_articles_path
      within 'select[name="q[tag_id]"]' do
        select 'Ruby'
      end
      click_button '検索'
      expect(page).to have_content(article_with_tag.title), 'タグでの検索ができていません'
      expect(page).not_to have_content(article_with_another_tag.title), 'タグでの絞り込みができていません'
    end
[spec/factories/articles.rb]

  factory :article do
    sequence(:title) { |n| "title-#{n}" }
    sequence(:slug) { |n| "slug-#{n}" }
    category
  end

  trait :with_tag do  ②
    transient do
      sequence(:tag_name) { |n| "test_tag_name_#{n}" }
      sequence(:tag_slug) { |n| "test_tag_slug_#{n}" }
    end

    after(:build) do |article, evaluator|     ③
      article.tags << build(:tag, name: evaluator.tag_name, slug: evaluator.tag_slug)
    end
  end

上記のようなテストコードがある場合の動きは、以下になる。
また、articleとtagのアソシエーションは、以下のようになる

class Article < ApplicationRecord
  has_many :article_tags
end

class Tag <ApplicationRecord
  has_many :articles, through: :article_tags
end

①でarticleがcreateされ、その時に(trait :with_tag)が指定されているので②が実行される。
[transient]は、属性に存在しないものを使いたい時に使う
③は、after(:build)でテスト用のデータを作成した後(buildになっているのは、railsでcreateするというのは、build→saveになる)に実行されるという事
③の中の部分は、以下のような動きになっている
article.tagsでアソシエーションを使って、生成したarticleに関連するtagという事
上記のarticle.tagsは、has_manyでアソシエーションされているので配列になるので、<<で配列に追加するイメージ(関連がbelongs_toの場合は、article.tagになると思うが、これは配列ではないので<<ではなく=で代入してあげるような記述になる)
build(:tag)でtagを生成して、その引数の(name: evaluator.tag_name, slug: evaluator.tag_slug)は、evaluator.tag_nameでtransientで生成した変数にアクセスしているようになる(下記がイメージ図)

   64:       sequence(:tag_slug) { |n| "test_tag_slug_#{n}" }
   65:     end
   66: 
   67:     after(:build) do |article, evaluator|
   68:       byebug
=> 69:       article.tags << build(:tag, name: evaluator.tag_name, slug: evaluator.tag_slug)
   70:     end
   71:   end
   72: 
   73:   trait :with_sentence do
(byebug) evaluator.tag_name
"Ruby"     #  [test_tag_name_1]でないのは、①で[tag_name]を"Ruby"でオーバーライドしているため
(byebug) evaluator.tag_slug
"test_tag_slug_1"

[ドライバの種類]
Rspecで利用するドライバは、以下の3種類がある

・driven_by(:rack_test)
・driven_by(:selenium_chrome)
・driven_by(:selenium_chrome_headless)

それぞれの特徴は、以下のようになる

driven_by(:rack_test)
javascriptを使っている動作の確認ができない
httpステータスコードを確認するテストができる

driven_by(:selenium_chrome)
javascriptを含むテストを実現できる
このドライバを使用するとブラウザが実際に立ち上がり、画面上で動きが確認できる
httpステータスコードの確認ができない

driven_by(:selenium_chrome_headless)
javascriptを含むテストを実現できる
selenium_chromeのようにブラウザ上での動きが確認できない
httpステータスコードの確認ができない

上記のことから、通常時はjavascriptも実現できるselenium_chromeを使って、httpステータスコードを確認したい部分だけrack_testを使う

[テストファイルの作成]

下記のコマンドでテストファイルを作成できる

システムスペック(システムテストのファイル)
$ rails g rspec:system ファイル名

モデルスペック(モデルテストのファイル)
$ rails g rspec:model ファイル名

参考記事:

【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の実行されるタイミング.|izumi haruya|note

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

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

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

https://nyoken.com/rspec-feature-capybara

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

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

http://one-way.tech/programing/capybara-spec/

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初心者個人的メモ

https://twei-blog.com/programming/ruby-on-rails/rspec/rspec-dialog/

have_fieldマッチャで指定した入力フィールドを確認する|haracaneのブログ

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

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

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

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

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

Headless Chromeを試してみる - Qiita

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

SystemSpecでのdate_fieldへの入力方法

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

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

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

Capybara + rspec で selected の状態を判定する : Rails - Qiita

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

FactoryBot(FactoryGirl)チートシート - Qiita

https://twei-blog.com/programming/ruby-on-rails/rspec/rspec-picture/

[RSpec] System Specでおしゃれラジオボタンをチェックできないときの対処法 - Qiita

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

RSpecでActionMailerのテスト - 技術メモ

以下の記事が凄く分かりやすく見やすい Capybaraチートシート - Qiita