Programming Journal

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

【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