【Rails】RSpecを利用したコントローラー単体テスト

# はじめに
コントローラーの単体テストやRSpecの利用方法を勉強中です。
メモも兼ねてQiitaに投稿しました。

# 実行環境
- rails 6.0.0
- ruby  2.5.6
- RSpec 4.0.0

# RSpecのインストール
テストコードはRSpec,Factorybotを利用するため、以下をインストール

```
  gem 'rspec-rails', '~> 4.0.0'
  gem 'factory_bot_rails'
```

# コントローラーの役割
テストコードを作成するにあたって、コントローラーの役割を一度振り返る。
コントローラー内のアクションがリクエストを受け取ったあと処理(データが必要であればモデル等と連携)を行い、レスポンスを返す。

なので、以下をベースにテストコードを記述します。

- 受信したリクエストに対して適切なレスポンスを返す
- データ処理が必要な場合はモデルと連携する
- レスポンスを表示するのに適切なビューやリダイレクト先を選択する

# テストコード記載

本記事ではGroupsというコントローラーのテストコードを記述します。

### 事前準備
FactoryBot作成

```
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name                  {'testuser'}
    provider              {'cognito-idp'}
    uid                   {'1111aaa-00zz-1111-0000-1a1a1a1a1a1a'}
    nickname              {'testuser'}

    trait :user_with_groups do
      after(:build) do |user|
        user.groups << create(:group)
      end
    end
  end
end
```
必要なカラムを記述。
アソシエーション先のGroupテーブルを纏めて作成する場合は、
`trait :user_with_groups`を利用します。

```
# spec/factories/groups.rb
FactoryBot.define do
  factory :group do
    name                  {'testgroup'}
    summary               {'テスト用グループ'}

    trait :group_with_users do
      after(:build) do |group|
        group.users << create(:user)
      end
    end

    trait :invalid do
      name                {''}
    end

    trait :updategroup do
      name                {'updategroup'}
    end
  end
end
```
`:invalid`と`:updategroup`はcreateやupdateアクション時に利用します。


### Before

```
# spec/factories/groups_spec.rb
RSpec.describe "Groups", type: :request do
let(:user)  { FactoryBot.create(:user, :user_with_groups) }
  before do
    allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return({ user_id: user.id })
  end
```
Groupsコントローラーはsign_inが前提となるため、ユーザーを作成。
`:user_with_groups`を指定して、関連するグループも作成しておきます。
`allow_any_instance_of`でダミーのsessionメソッドを作成します。

### GET #index
```
# spec/factories/groups_spec.rb
describe 'GET #index' do
    before do
      get groups_path
    end
    it 'indexアクションにリクエストすると正常にレスポンスが返ってくる' do
      expect(response.status).to eq(200)
    end
    it 'index.html.erbにユーザー名が表示される' do
      expect(response.body).to include 'testuser'
    end
  end
```
HTTPステータスコードと、ビューファイルにユーザー名が表示されているかを確認

### GET #new
```
# spec/factories/groups_spec.rb
describe 'GET #new' do
    before do
      get new_group_path
    end
    it 'newアクションにリクエストすると正常にレスポンスが返ってくる' do
      expect(response.status).to eq(200)
    end
  end
```
indexと同様、HTTPステータスコードを確認

### POST #create
```
# spec/factories/groups_spec.rb
  describe 'POST #create' do
    context '有効なパラメーターの場合' do
      let(:group_params) { FactoryBot.attributes_for(:group, :group_with_users) }
      it 'createアクションにリクエストすると正常にリダイレクトされる' do
        post groups_path, params: { group: group_params }
        expect(response.status).to eq(302)
      end
      it 'テーブルに新しいデータが登録される' do
        expect{
          post groups_path, params: { group: group_params }
        }.to change(Group, :count).by(1)
      end
      it 'indexアクションにリダイレクトされる' do
        post groups_path, params: { group: group_params }
        expect(response).to redirect_to(groups_path)
      end
    end

    context '無効なパラメーターの場合' do
      let(:group_params) { FactoryBot.attributes_for(:group, :group_with_users, :invalid) }
      it 'createアクションにリクエストすると正常にレスポンスが返ってくる' do
        post groups_path, params: { group: group_params }
        expect(response.status).to eq 200
      end
      it 'テーブルに新しいデータが登録されない' do
        expect{
          post groups_path, params: { group: group_params }
        }.to change(Group, :count).by(0)
      end
      it 'new.html.erbにエラーが表示される' do
        post groups_path, params: { group: group_params }
        expect(response.body).to include 'can&#39;t be blank'
      end
    end
  end
```
HTTPステータス、テーブルの更新有無、リダイレクト先、エラーの表示を確認。
`FactoryBot.attributes_for`でハッシュパラメーターを生成。
無効なパラメーターの場合は、`trait`で定義した`:invalid`を呼び出してnameを空で作成。


### GET #show
```
# spec/factories/groups_spec.rb
describe 'GET #show' do
    before  do
      get group_path(group_id)
    end
    context '参加しているグループが指定された場合' do
      let(:group_id) { user.groups.ids[0] }
      it 'showアクションにリクエストすると正常にレスポンスが返ってくる' do
        expect(response.status).to eq(200)
      end
      it 'show.html.erbにグループ名が表示される' do
        expect(response.body).to include 'testgroup'
      end
    end
    context '参加していないグループが指定された場合' do
      let(:group_id) { 2 }
      it 'showアクションにリクエストすると正常にリダイレクトされる' do
        expect(response.status).to eq(302)
      end
      it 'indexアクションにリダイレクトされる' do
        expect(response).to redirect_to(groups_path)
      end
    end
    context '存在しないグループが指定された場合' do
      let(:group_id) { 999999 }
      it 'showアクションにリクエストすると正常にリダイレクトされる' do
        expect(response.status).to eq(302)
      end
      it 'indexアクションにリダイレクトされる' do
        expect(response).to redirect_to(groups_path)
      end
    end
  end
```
HTTPステータス、グループ名の表示、リダイレクト先を確認。
contextに応じてgroup_idを変えています。
`user.groups.ids[0]`はgroups_spec.rbの冒頭で関連するテーブルを作成済なので、idを代入してます。
存在しないグループが指定された場合のケースは、`ActiveRecord::RecordNotFound`の例外処理で、groups_pathへリダイレクトする様にしているので、それを想定したテストとなります。

### GET #edit
```
# spec/factories/groups_spec.rb
describe 'GET #edit' do
    before do
      get edit_group_path(group_id)
    end
    context '参加しているグループが指定された場合' do
      let(:group_id) { user.groups.ids[0] }
      it 'editアクションにリクエストすると正常にレスポンスが返ってくる' do
        expect(response.status).to eq(200)
      end
      it 'edit.html.erbにユーザー名が表示される' do
        expect(response.body).to include 'testuser'
      end
      it 'edit.html.erbにグループ名が表示される' do
        expect(response.body).to include 'testgroup'
      end
    end
    context '参加していないグループが指定された場合' do
      # 省略
    end
    context '存在しないグループが指定された場合' do
      # 省略
    end
  end
```
他のアクション同様、HTTPステータス、ユーザー名、グループ名の表示、リダイレクト先を確認。
`# 省略`部分はshowアクションと同様です。

### PUT #update
```
# spec/factories/groups_spec.rb
describe 'PUT #update' do
    context '有効なパラメーターの場合' do
      let(:group_id) { user.groups.ids[0] }
      it 'updateアクションにリクエストすると正常にリダイレクトされる' do
        put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :updategroup) }
        expect(response.status).to eq(302)
      end
      it 'テーブルが更新される' do
        expect{
          put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :updategroup) }
        }.to change { Group.find(group_id).name }.from('testgroup').to('updategroup')
      end
      it 'showアクションにリダイレクトされる' do
        put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :updategroup) }
        expect(response).to redirect_to(group_path(group_id))
      end
    end
    context '無効なパラメーターの場合' do
      let(:group_id) { user.groups.ids[0] }
      it 'updateアクションにリクエストすると正常にレスポンスが返ってくる' do
        put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :invalid) }
        expect(response.status).to eq(200)
      end
      it 'テーブルが更新されない' do
        expect{
          put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :invalid) }
        }.to_not change(Group.find(group_id), :name)
      end
      it 'edit.html.erbにエラーが表示される' do
        put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :invalid) }
        expect(response.body).to include 'can&#39;t be blank'
      end
    end
    context '参加していないグループが指定された場合' do
      # 省略
    end
    context '存在しないグループが指定された場合' do
      # 省略
    end
  end
```
createアクションとほぼ同じで、HTTPステータス、テーブルの更新有無、リダイレクト先、エラーの表示を確認。
contextに応じて`:updategroup`と`:invalid`を利用してハッシュパラメーターを作成し、リクエストしてます。
`# 省略`部分はshow,editアクションと同様です。

# 要確認
- allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return({ user_id: user.id })
 - allow_any_instance_ofの利用は非推奨らしい。
 - SessionsHelper内で作成したsign_inメソッドを利用する想定でしたが、sessionのNoMethoderrorがでてしまったため、このやり方に行きつきました。
 - 他の方法がないか、継続調査。
- destroyのテスト
    - 今回は削除の要件がなく実施しておりませんが、必要なタイミングで調べたい。
- createとupdateで一部冗長な記述があるのでリファクタリングする。

# 参考記事
https://qiita.com/blueberrystream/items/884d0d23caf50b6553a0
https://qiita.com/jnchito/items/42193d066bd61c740612
https://qiita.com/t2kojima/items/ad7a8ade9e7a99fb4384