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