ぺんぎんのRails日記

ぺんぎん。エンジニア経験ゼロ。Rails勉強中。

【RSpec】システムスペック【Rails】

システムスペックとは

前回の記事で扱ったモデルスペックは単一のモデルの動作をチェックする単体テストだったのに対し、今回のシステムスペックは、プログラム全体の挙動を確認するための統合テストのひとつです。

システムスペックはCapybaraを使ってブラウザの操作をシミュレートすることができるテストです。

Capybaraの準備

Gemfile

gem 'capybara'

Gemfileに書いたらいつも通りbundle installします。

テスト要件の確認

spec/system/user_spec.rb

RSpec.describe 'Users', type: :system do
  describe 'ログイン前' do
    describe 'ユーザー新規登録' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの新規作成が成功する'
      end
      context 'メールアドレスが未入力' do
        it 'ユーザーの新規作成が失敗する'
      end
      context '登録済のメールアドレスを使用' do
        it 'ユーザーの新規作成が失敗する'
      end
    end
 
    describe 'マイページ' do
      context 'ログインしていない状態' do
        it 'マイページへのアクセスが失敗する'
      end
    end
  end
 
  describe 'ログイン後' do
    describe 'ユーザー編集' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの編集が成功する'
      end
      context 'メールアドレスが未入力' do
        it 'ユーザーの編集が失敗する'
      end
      context '登録済のメールアドレスを使用' do
        it 'ユーザーの編集が失敗する'
      end
      context '他ユーザーの編集ページにアクセス' do
        it '編集ページへのアクセスが失敗する'
      end
    end
 
    describe 'マイページ' do
      context 'タスクを作成' do
        it '新規作成したタスクが表示される'
      end
    end
  end
end

Loginメソッドをmodule化する

今回は上記のuser_spec以外にuser_sessions_spec、task_specでもloginメソッドが必要になるため、moduleとして切り出してまとめてしまいます。

rails_helper.rbで設定を変更する

デフォルトでは、23行目あたりに以下コードがコメントアウトされています。 これを有効にするためにコメントアウトを解除します。

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

さらに、moduleとしてこのあと作成するlogin_supportファイルを読み込んで欲しいので、RSpec.configure do |config|以下に下のコードを追加します。

  config.include LoginSupport

②login_support.rbファイルを作る

moduleを記載するファイルとしてspecディレクトリ内にsupport/login_support.rbファイルを作成します。

spec/support/login_macros.rb

module LoginMacros
  def login_as(user)
    visit root_path
    click_link 'Login'
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password'
    click_button 'Login'
  end
end


これでloginメソッドを一括化することができました( ^ω^ )

では、早速このloginメソッドをテストコードに入れ込んでいきましょう。 ログインの処理なので、'ログイン後'の部分に入れていきます!
spec/system/users_spec.rb

RSpec.describe 'Users', type: :system do
...
  describe 'ログイン後' do
    before { login_as(user) }
...   

これで'ログイン後'内でログイン状態を持続させることができました!

要件の中身を書こう!

では、中身を書いていきます。

descrive 'ログイン前' do

まず正常系の'ユーザーの新規作成が成功する'は、こんな構成で書いていきます。

describe 'ユーザー新規登録' do
  context 'フォームの入力値が正常' do
    it 'ユーザーの新規作成が成功する' do
      ユーザー新規作成ページにアクセス
      メールアドレスを入力する
      パスワードを入力する
      確認用パスワードを入力する
      SignUpボタンを押す
      'User was successfully created.'がページに表示される
      現在のパスがログイン画面のパスになっている
    end
  end
end

・「特定のページにアクセスする」ときの検証方法はvisit パス名を使います。

visit new_user_path

・「入力する」ときはfill in '項目', with '入力内容'です。

fill_in 'Email', with: 'email@example.com'
fill_in 'Password', with: 'password'
fill_in 'Password confirmation', with: 'password'

・「ボタンを押す」はclick 'ボタンのラベル'。

click_button 'SignUp'

・「ページに何かが表示されている」かどうかの検証はexpect(page).to have_contentを使います。

expect(page).to have_content 'User was successfully created.'

・期待したページに遷移できたかどうかの検証は、「現在のページが特定のページと同じであるか」という検証になります。current_pathを使っていきます。

expect(current_path).to eq login_path

次に異常系の'メールアドレスが未入力'を書きます。構成は次の通りです。

describe 'ログイン前' do
    describe 'ユーザー新規登録' do
     ...
      context 'メールアドレスが未入力' do
        it 'ユーザーの新規作成が失敗する' do
          visit new_user_path
          メールアドレスの入力が空
          ...
          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
  end
end

・fill_inで未入力を検証するときはwith以降を''と空にするか、nilを入れます。

fill_in 'Email', with: ''

次は登録済みのメールアドレスを使った時にユーザーの新規作成に失敗することを検証します。

describe 'ログイン前' do
    describe 'ユーザー新規登録' do
     ...
      context '登録済のメールアドレスを使用' do
        it 'ユーザーの新規作成が失敗する' do
          存在するユーザーを定義
          visit new_user_path
          存在するメールアドレスを入力
          ...
          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 users_path
          フォームに入力した無効なメールアドレスが入っている
        end
      end
    end
end

・存在するユーザーを定義

existed_user = FactoryBot.create(:user)

・存在するメールアドレスを入力 先ほど定義した変数でメールアドレスを入力します。

fill_in 'Email', with: existed_user.email

・フォームに入力した無効なメールアドレスが入っている これは新しいですね。「ページに表示がある」に似ていますが、have_contentではなくフォーム内に値があるかどうかなのでhave_fieldを使います。

expect(page).to have_field 'Email', with: existed_user.email

最後に、ログインしていない状態でマイページにアクセスできないことを検証します。 userを定義した後で、userのマイページにアクセスし、エラーメッセージが表示されてログイン画面にrenderするという流れです。

describe 'ログイン前' do
     ...
    describe 'マイページ' do
      context 'ログインしていない状態' do
        it 'マイページへのアクセスが失敗する' do
          user = FactoryBot.create(:user)
          visit user_path(user)
          expect(page).to have_content('Login required')
          expect(current_path).to eq login_path
        end
      end
    end
  end

describe 'ログイン後' do

次に、ログイン後の検証です。
書き方はログイン前のときと同じような感じですので、説明は割愛しますが、次のコードを見て見ましょう。

  describe 'ログイン後' do
    describe 'ユーザー編集' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの編集が成功する' do
          user = FactoryBot.create(:user)
          visit edit_user_path(user)
          fill_in 'Email', with: 'update@example.com'
          fill_in 'Password', with: 'update_password'
          fill_in 'Password confirmation', with: 'update_password'
          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
          user = FactoryBot.create(:user)
          visit edit_user_path(user)
          fill_in 'Email', with: ''
          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 can't be blank")
          expect(current_path).to eq user_path(user)
        end
      end

      context '登録済のメールアドレスを使用' do
        it 'ユーザーの編集が失敗する' do
          user = FactoryBot.create(:user)
          visit edit_user_path(user)
          other_user = FactoryBot.create(: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
          user = FactoryBot.create(:user)
          other_user = FactoryBot.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
          user = FactoryBot.create(:user)
          FactoryBot.create(:task, title: 'test_title', status: :doing, user: user)
          visit user_path(user)

これを見ると、userという変数の定義を何度も行なっています。
DRYにしましょう!
前回の記事で、beforeをひとつ上の階層で書くと共通化できると説明しましたが、let(:変数名) { 内容 }で共通化することもできます。こんな感じです。

  describe 'ログイン後' do
    let(:user) { FactoryBot.create(:user) }
    describe 'ユーザー編集' do
      context 'フォームの入力値が正常' do
        it 'ユーザーの編集が成功する' do
          visit edit_user_path(user)
          fill_in 'Email', with: 'update@example.com'
          fill_in 'Password', with: 'update_password'
          fill_in 'Password confirmation', with: 'update_password'
          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 edit_user_path(user)
          fill_in 'Email', with: ''
          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 can't be blank")
          expect(current_path).to eq user_path(user)
        end
      end

      context '登録済のメールアドレスを使用' do
        it 'ユーザーの編集が失敗する' do
          visit edit_user_path(user)
          other_user = FactoryBot.create(: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
          other_user = FactoryBot.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
          FactoryBot.create(:task, title: 'test_title', status: :doing, user: user)
          visit user_path(user)
          expect(page).to have_content('You have 1 task.')
          expect(page).to have_content('test_title')
          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

ちなみにログイン前の検証でも同じ変数を使っている部分があったので、さらに上の階層でletを書いてもいいですね!

今回は省略しましたが、こんな感じで同じシステムスペックのtasks_specとuser_sessionsも書いていきましょうー!