【Ruby on Rails】結合テストコードについてまとめ(Capybara)

初学者です。
テストコードがなぜか大好きです。

今回は結合テストコードについてまとめます。
Capybaraという標準で導入されているGemを利用しています。


## 前提条件

- FactoryBotを導入済みである

上記については以下の記事にまとめています。
[【Ruby on Rails】FactoryBotとFakerについてまとめ](https://qiita.com/mumucochimu/items/10432fd5173e63ebd418)


## テストコードの種類

- ### 単体テストコード
モデルやコントローラーなど`機能ごと`に問題がないか確認します。
例えばバリデーションがきちんと動作しているかなどです。


- ### 結合テストコード
ユーザーがブラウザで操作する`一連の流れ`を再現して問題がないか確かめます。
例えば、ユーザー登録で「名前とメールアドレスとパスワードを入力するとトップページに遷移して表示がユーザーの名前に変わっている」などの一連の動作を確認します。


- ### 正常系
`ユーザーが開発者の意図する操作を行った時の挙動`を確認するテストコードです。
例えば、ユーザー登録で問題なく全てのデータが入力された場合などです。

- ### 異常系
`ユーザーが開発者の意図しない操作を行った時の挙動`を確認するテストコードです。
例えば、ユーザー登録で正しい値が入っていないと登録できないかどうかなどです。


## 準備

今回はUserモデルの結合テストコードを例にしていきます。

まずは下記コマンドで結合テストコードを書くファイルを準備します。

```Ruby:ターミナル
rails g rspec:system users
```

`spec/system`配下に`users_spec.rb`というファイルが生成されていればOKです。


## テストコード

解説はあとにして先に記述例を載せます。

```Ruby:spec/system/users_spec.rb
require 'rails_helper'

RSpec.describe 'ユーザー新規登録', type: :system do
  before do
    @user = FactoryBot.build(:user)
  end
  context 'ユーザー新規登録ができるとき' do 
    it '正しく情報を入力すればユーザー新規登録ができてトップページに移動する' do
      # トップページに移動する
      visit root_path
      # トップページに新規登録ページへ遷移するボタンがあることを確認する
      expect(page).to have_content('新規登録')
      # 新規登録ページへ移動する
      visit new_user_registration_path
      # ユーザー情報を入力する
      fill_in 'name', with: @user.nickname
      fill_in 'Email', with: @user.email
      fill_in 'Password', with: @user.password
      fill_in 'Password confirmation', with: @user.password_confirmation
      # 登録ボタンを押すとユーザーモデルのカウントが1増えることを確認する
      expect{
        find('input[name="button"]').click
      }.to change { User.count }.by(1)
      # トップページへ遷移することを確認する
      expect(current_path).to eq(root_path)
      # カーソルを合わせるとログアウトボタンが表示されることを確認する
      expect(
        find('.user_nav').find('span').hover
      ).to have_content('ログアウト')
      # 新規登録ページへ遷移するボタンやログインページへ遷移するボタンが表示されていないことを確認する
      expect(page).to have_no_content('新規登録')
      expect(page).to have_no_content('ログイン')
    end
  end
  context 'ユーザー新規登録ができないとき' do
    it '誤った情報ではユーザー新規登録ができずに新規登録ページへ戻ってくる' do
      # トップページに移動する
      visit root_path
      # トップページに新規登録ページへ遷移するボタンがあることを確認する
      expect(page).to have_content('新規登録')
      # 新規登録ページへ移動する
      visit new_user_registration_path
      # ユーザー情報を入力する
      fill_in 'name', with: ''
      fill_in 'Email', with: ''
      fill_in 'Password', with: ''
      fill_in 'Password confirmation', with: ''
      # 登録ボタンを押してもユーザーモデルのカウントが増えないことを確認する
      expect{
        find('input[name="button"]').click
      }.to change { User.count }.by(0)
      # 新規登録ページへ戻されることを確認する
      expect(current_path).to eq('/users')
    end
  end
end

RSpec.describe 'ログイン', type: :system do
  before do
    @user = FactoryBot.create(:user)
  end
  context 'ログインができるとき' do
    it '保存されているユーザーの情報と合致すればログインができる' do
      # トップページに移動する
      visit root_path
      # トップページにログインページへ遷移するボタンがあることを確認する
      expect(page).to have_content('ログイン')
      # ログインページへ遷移する
      visit new_user_session_path
      # 正しいユーザー情報を入力する
      fill_in 'Email', with: @user.email
      fill_in 'Password', with: @user.password
      # ログインボタンを押す
      find('input[name="button"]').click
      # トップページへ遷移することを確認する
      expect(current_path).to eq(root_path)
      # カーソルを合わせるとログアウトボタンが表示されることを確認する
      expect(
        find('.nav_bar').find('span').hover
      ).to have_content('ログアウト')
      # サインアップページへ遷移するボタンやログインページへ遷移するボタンが表示されていないことを確認する
      expect(page).to have_no_content('新規登録')
      expect(page).to have_no_content('ログイン')
    end
  end
  context 'ログインができないとき' do
    it '保存されているユーザーの情報と合致しないとログインができない' do
      # トップページに移動する
      visit root_path
      # トップページにログインページへ遷移するボタンがあることを確認する
      expect(page).to have_content('ログイン')
      # ログインページへ遷移する
      visit new_user_session_path
      # ユーザー情報を入力する
      fill_in 'Email', with: ''
      fill_in 'Password', with: ''
      # ログインボタンを押す
      find('input[name="button"]').click
      # ログインページへ戻されることを確認する
      expect(current_path).to eq(new_user_session_path)
    end
  end
end
```

まず`describe`で`新規登録の場合とログインの場合`で項目分けします。

次に`context`でそれぞれ`正常系と異常系`で項目分けします。

そのあと、それぞれの流れを書いていきます。

`FactoryBot`については新規登録は`build`ですが、ログインはユーザー情報があらかじめあることが前提なので`create`にしています。

では内容について細かく解説していきます。


### visit

`トップページに移動する`を`visit root_path`としています。
visitは`遷移する`という意味で利用します。

### page

visitで訪れた先のページの情報が格納されています。例えばカーソルを合わせてはじめて見ることができる文字列はpageの中に含まれません。

### have_content

`トップページに新規登録ページへ遷移するボタンがあることを確認する`を`expect(page).to have_content('新規登録')`としています。
visitで訪れたpageの中に該当の文字列があるかどうかを判断する時に利用します。

### fill_in

`ユーザー情報を入力する`は`fill_in`を利用しています。
`fill_in フォームの名前, with: 入力する文字列`と記述することでフォームへの入力を行うことができます。

### find().click

`find(クリックしたい要素).click`と記述することでクリックの動作ができます。

`登録ボタンを押すとユーザーモデルのカウントが1増えることを確認する`を`登録ボタンを押す`の動作を`find('input[name="button"]').click`で表しています。
この場合は`input`要素の`name="button"`を指定していることになります。

### change

`expect{ 動作 }.to change { モデル名.count }.by(1)`で`登録ボタンを押すとユーザーモデルのカウントが1増えることを確認する`の
`カウントが1増える`を表しています。


### current_path

`トップページへ遷移することを確認する`を`expect(current_path).to eq(root_path)`としています。
`current_path`は`今いるページ`という意味です。

### hover

`find(要素).hover`で特定の要素にカーソルをあわせたときの動作を表すことができます。
`カーソルを合わせるとログアウトボタンが表示されることを確認する`を`expect(find('.nav_bar').find('span').hover).to have_content('ログアウト')`としています。
今回の場合は`.nav_bar`の中の`span`要素という感じで二重に探しています。


### have_no_content

`サインアップページへ遷移するボタンやログインページへ遷移するボタンが表示されていないことを確認する`を`expect(page).to have_no_content('新規登録')`と`expect(page).to have_no_content('ログイン')`としています。
存在しないことを確認するという意味です。


以上です。