【Rails6】RSpecによる投稿、及び回答時の結合テストの実装

#はじめに
サービスの品質を保つために必要不可欠なテストを実施しております。

今回は投稿、及び回答時の結合テストを実装し、その実装内容を記事にしていきたいと思います。

#前提

投稿、及び回答時のモデル単体テストは実施済み

※完了されていない方や単体テストと並行してご覧になられたい方は、以下の投稿時のモデルの単体テストについての記事をご参考ください。

https://qiita.com/narimiya/items/301dd9a10f32e0061cff

#バージョン
rubyのバージョン　ruby-2.6.5
Railsのバージョン Rails:6.0.0
rspec-rails 4.0.0

#実施したテスト

###投稿時の結合テスト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1052793/56a06d1c-9b16-5199-f662-de72a7f4455e.png)

###回答時の結合テスト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1052793/197d63b1-dee5-efe5-94e6-0a3b8aafa4bf.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1052793/9e7c2c2d-5f01-fbc7-ab4b-1324d5416238.png)

#definitionsテーブルのカラムの紹介

```xxx_create_definitions.rb
class CreateDefinitions < ActiveRecord::Migration[6.0]
  def change
    create_table :definitions do |t|
      t.text       :title,          null: false
      t.text       :body,           null: false
      t.references :user,           foreign_key: true
      t.timestamps
    end
  end
end
```

#answersテーブルのカラムの紹介

```xxx_create_answers.rb
class CreateAnswers < ActiveRecord::Migration[6.0]
  def change
    create_table :answers do |t|
      t.text       :answer,     null: false
      t.references :user,       foreign_key: true
      t.references :definition, foreign_key: true
      t.timestamps
    end
  end
end
```

#Definitionモデルのバリデーション

```app/models/definition.rb
class Definition < ApplicationRecord
  belongs_to :user
  has_many :answers, dependent: :destroy
  has_many :reviews, dependent: :destroy
  has_many :notifications, dependent: :destroy
  has_many :likes, dependent: :destroy

  validates :title, presence: true
  validates :body, presence: true

# 〜省略〜
```

#Answerモデルのバリデーション

```app/models/answer.rb
class Answer < ApplicationRecord
  belongs_to :user
  belongs_to :definition
  has_many :likes, dependent: :destroy
  has_many :notifications, dependent: :destroy


  validates :answer, presence: true

# 〜省略〜
```

#FactoryBotの内訳（definition）

```spec/factories/definitions.rb
FactoryBot.define do
  factory :definition do
    title                        {Faker::Lorem.sentence}
    body                         {Faker::Lorem.sentence}
    association :user
  end
end
```

#FactoryBotの内訳（answer）

```spec/factories/answers.rb
FactoryBot.define do
  factory :answer do
    answer                        {Faker::Lorem.word}
    user_id                       { FactoryBot.create(:user).id }
    definition_id                 { FactoryBot.create(:definition).id }
  end
end
```

#サポートモジュール

```spec/support/sign_in_support.rb
module SignInSupport
  def sign_in(user)
    visit user_session_path
    fill_in 'user[email]', with: user.email
    fill_in 'user[password]', with: user.password
    find('input[name="commit"]').click
    expect(current_path).to eq(root_path)
  end
end
```

#テストコードの内容
###投稿時の結合テスト

```spec/system/definitions_spec.rb
require 'rails_helper'

RSpec.describe "Definitions", type: :system do
  before do
    @user = FactoryBot.create(:user)
    @definition_title = Faker::Lorem.sentence
    @definition_body = Faker::Lorem.sentence
  end
  context '投稿ができるとき'do
    it 'ログインしたユーザーは新規投稿できる' do
    # ログインする
    sign_in(@user)

    # 新規投稿ページへのリンクがあることを確認する
    expect(page).to have_content('投稿')

    # 投稿作成ページに移動する
    visit new_definition_path

    # フォームに情報を入力する
    fill_in 'definition[title]', with:@definition_title
    fill_in 'definition[body]', with: @definition_body

    # 送信するとDefinitionモデルのカウントが1上がることを確認する
    expect{
      find('input[name="commit"]').click
    }.to change { Definition.count }.by(1)

    # 投稿完了ページに遷移することを確認する
    expect(current_path).to eq(root_path)

    # トップページに遷移する
    visit root_path

    # トップページには先ほど投稿した内容の投稿が存在することを確認する
    expect(page).to have_content(@definition_title)
    end
  end
  context '投稿ができないとき'do
    it 'ログインしていないと新規投稿ページに遷移できない' do
       # トップページに遷移する
       visit root_path

       # 新規投稿ページへのリンクがない
       expect(page).to have_no_content('投稿')
    end
  end
end

RSpec.describe '投稿の編集', type: :system do
  before do
    @definition1 = FactoryBot.create(:definition)
    @definition2 = FactoryBot.create(:definition)
  end
  context '投稿の編集ができるとき' do
      it 'ログインしたユーザーは自分が投稿した投稿の編集ができる' do
        # definition1を投稿したユーザーでログインする
        sign_in(@definition1.user)

        # 投稿ページに移動する
        visit definition_path(@definition1.id)

        # definition1に「編集」ボタンがあることを確認する
        expect(page).to have_content('編集する'), href: edit_definition_path(@definition1)

        # 編集ページへ遷移する
        visit edit_definition_path(@definition1)

        # すでに投稿済みの内容がフォームに入っていることを確認する
        expect(
          find('#definition_title').value
        ).to eq(@definition1.title)
        expect(
          find('#definition_body').value
        ).to eq(@definition1.body)

        # 投稿内容を編集する
        fill_in 'definition_title', with: "#{@definition1.title}+編集したタイトル"
        fill_in 'definition_body', with: "#{@definition1.body}+編集した詳細"

        # 編集してもdefinitiontモデルのカウントは変わらないことを確認する
        expect{
          find('input[name="commit"]').click
        }.to change { Definition.count }.by(0)

        # 編集完了画面に遷移したことを確認する
        expect(current_path).to eq(root_path(@definition1))

        # トップページには先ほど変更した内容の投稿が存在することを確認する
        expect(page).to have_content("#{@definition1.title}+編集したタイトル")
    end
  end
  context '投稿の編集ができないとき' do
      it 'ログインしたユーザーは自分以外が投稿した投稿の編集画面には遷移できない' do
        # ログインする
        sign_in(@definition1.user)

        # definition2を投稿したユーザーとして投稿ページに遷移する
        visit definition_path(@definition2.id)

        # 「編集」ボタンがないことを確認する
        expect(page).to have_no_content('編集する'), href: edit_definition_path(@definition1)
      end
      it 'ログインしていないと投稿の編集画面には遷移できない' do
        # 投稿ページに移動する
        visit definition_path(@definition1.id)

        # definition1に「編集」ボタンがないことを確認する
        expect(page).to have_no_content('編集する'), href: edit_definition_path(@definition1)
      end
  end
end

RSpec.describe '投稿の削除', type: :system do
  before do
    @definition1 = FactoryBot.create(:definition)
    @definition2 = FactoryBot.create(:definition)
  end
    context '投稿の削除ができるとき' do
        it 'ログインしたユーザーは自らが投稿した投稿の削除ができる' do
        # definition1を投稿したユーザーでログインする
        sign_in(@definition1.user)

        # 投稿ページに移動する
        visit definition_path(@definition1.id)

        # definition1に「削除」ボタンがあることを確認する
        expect(page).to have_content('削除する'), href: definition_path(@definition1)

        # 投稿を削除するとレコードの数が1減ることを確認する
        expect{
          find_link('削除する', href: definition_path(@definition1)).click }.to change { Definition.count }.by(-1)

        # 削除完了画面に遷移したことを確認する
        expect(current_path).to eq(root_path)

        # トップページにはdefinition1の内容が存在しないことを確認する（テキスト）
        expect(page).to have_no_content("#{@definition1.title}")
        end
  end
    context '投稿が削除ができないとき' do
        it 'ログインしたユーザーは自分以外が投稿した内容の削除ができない' do
          # definition1を投稿したユーザーでログインする
          sign_in(@definition1.user)

          # definition2を投稿したユーザーとして投稿ページに遷移する
          visit definition_path(@definition2.id)

          # definition2に「削除」ボタンが無いことを確認する
          expect(page).to have_no_content('削除する'), href: definition_path(@definition1)
        end
        it 'ログインしていないと投稿の削除ボタンがない' do
          # 投稿ページに移動する
          visit definition_path(@definition1.id)

          # definition1に「編集」ボタンがないことを確認する
          expect(page).to have_no_content('削除する'), href: definition_path(@definition1)
        end
    end
end
```

###回答時の結合テスト

```spec/system/answers_spec.rb
require 'rails_helper'

RSpec.describe "Answers", type: :system do
  before do
    @definition = FactoryBot.create(:definition)
    @answer = Faker::Lorem.sentence
  end

  it 'ログインしたユーザーは投稿詳細ページで回答できる' do
      # ログインする
      sign_in(@definition.user)

      # 投稿詳細ページに遷移する
      visit definition_path(@definition.id)

      # フォームに情報を入力する
      fill_in 'answer_answer', with: @answer

      # 回答を送信すると、Answerモデルのカウントが1上がることを確認する
      expect{
        find('input[name="commit"]').click
      }.to change { Answer.count }.by(1)

      # 詳細ページにリダイレクトされることを確認する
      expect(current_path).to eq(definition_path(@definition))
      
      # 詳細ページ上に先ほどの回答内容が含まれていることを確認する
      expect(page).to have_content @answer
  end
end
```

#補足説明

##サポートモジュールとは
メソッド等をまとめる機能（RSpecに搭載）
メソッドとして処理をまとめることができる。

##have_linkについて
expect('要素').to have_link 'ボタンの文字列', href: 'リンク先のパス'と記述することで、要素の中に当てはまるリンクがあることを確認できます。

have_linkはa要素に対して用います。

##have_no_linkについて
expect('要素').to have_no_link 'ボタンの文字列', href: 'リンク先のパス'と記述することで、要素の中に当てはまるリンクがないことを確認できます。

##find_link(~).clickについて
find_link('リンクの文字列', href: 'URL').clickといった形で使います。

a要素で表示されているリンクをクリックするために用います。

##テストコードの内容について

###投稿
**投稿について**
正常
・投稿が上手くいく場合
　ログインして投稿ができる

異常
・投稿が上手くいかない場合
　ログインしていない時は投稿はできない

**投稿の編集について**
正常
・編集が上手くいく場合
　ログインしていると編集ができる

異常
・編集が上手くいかない場合
　ログインしても自分以外が投稿した投稿の編集画面には遷移できない
　ログインしていない時は編集はできない

**投稿の削除について**
正常
・削除が上手くいく場合
　ログインしていると編集ができる

異常
・削除が上手くいかない場合
　ログインしても自分以外が投稿した投稿の削除ボタンが表れない
　ログインしていない時は削除はできない

###回答
正常
ログインしていると回答できる

異常
異常系は投稿内容のものと記述が同じため、可読性の観点から省略している。

以上です。