Railsチュートリアル(3章〜8章) request specを用いた実装

#はじめに

業務でRspecを使用するため、RailsチュートリアルのMinitestの実装部分をRspecを用いて実装した。（実装範囲はRailsチュートリアル第3章〜第8章）

RailsチュートリアルではControllerやModelに関するテストと統合テスト(Integration Test)がある。
このIntegrationTestの部分にはRspecのRequestSpecを用いた。

また、コントローラの機能テストに関しては、Rails5以降ではrequest specで記述することが推奨されているため、こちらもRequestSpecを用いて実装した。
#コントローラー機能テストのポイント
以下がcontroller specで行うべきテスト項目です。
> 
・Webリクエストが成功したか
・正しいページにリダイレクトされたか
・ユーザー認証が成功したか
・レスポンスのテンプレートに正しいオブジェクトが保存されたか
・ビューに表示されたメッセージは適切か

request specでも実装内容はほとんど同じであるが、
「レスポンスのテンプレートに正しいオブジェクトが保存されたか」は
コントローラの内部実装に関わる点はテストすべきではないとされている。
理由としてはrequest specはリクエスト/レスポンスにのみ関心を持つブラックボックステストであるといったところである。
#実装
Railsチュートリアルの第3章〜第8章では、
①Getメソッドでのページ（Home,About,Help,Contact,New）
②Postメソッドを用いたSignUp
③ログイン・ログアウトに関して
④Modelに関して
と、大きく分けて4つに分けられる。

まずは共通準備としてFactoryBotを使用。

```ruby:spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "DefaultName" }
    email { "defaultemail@gmail.com" }
    password {"foobar"}
    password_confirmation {"foobar"}
  end
end
```
後に使用するHelperを定義。

```ruby:app/helpers/users_helper.rb
module UsersHelper
    def sign_in_as(user)
      post login_path, params: { session: { email: user.email,password: user.password } }
    end
end
```

①に関してのRequestSpec
一つのitに一つのテストを行うことを推奨されているのでそれに従う。

```spec/requests/static_pages_apis_spec.rb
require 'rails_helper'

RSpec.describe "Getメソッドでのページ", type: :request do

  describe "GET #Home" do
    before do
      get root_path
    end
    it "Homeページのhttpリクエストは正しいか" do
      expect(response).to have_http_status(200)
    end
    it 'タイトルが正しく表示されているか' do
      expect(response.body).to include "Ruby on Rails Tutorial Sample App"
    end
  end

  describe "GET #About" do
    before do
      get about_path
    end
    it "Aboutページのhttpリクエストは正しいか" do
      expect(response).to have_http_status(200)
    end
    it 'タイトルが正しく表示されているか' do
      expect(response.body).to include "About"
    end
  end

  describe "GET #Help" do
    before do
      get help_path
    end
    it "Helpページのhttpリクエストは正しいか" do
      expect(response).to have_http_status(200)
    end
    it 'タイトルが正しく表示されているか' do
      expect(response.body).to include "Help"
    end
  end

  describe "GET #Contact" do
    before do
      get contact_path
    end
    it "Contactページのhttpリクエストは正しいか" do
      expect(response).to have_http_status(200)
    end
    it 'タイトルが正しく表示されているか' do
      expect(response.body).to include "Contact"
    end
  end

  describe "GET #New" do
    before do
      get signup_path
    end
    it "User/newページのhttpリクエストは正しいか" do
      expect(response).to have_http_status(200)
    end
    it 'タイトルが正しく表示されているか' do
      expect(response.body).to include "Sign up"
    end
  end
end

```
続いて、②の新規登録に関してのRequestSpec
途中出てくる`params: { user: FactoryBot.attributes_for(:user,name: '') }`でだいぶ苦戦したのでここについて後述します。※1

```ruby:spec/requests/users_signup_apis_spec.rb
require 'rails_helper'

RSpec.describe "SignUpに関して", type: :request do
  describe "POST #create" do
    context 'パラメータが妥当' do
      it "リクエストが成功すること" do
        post signup_path, params: { user: FactoryBot.attributes_for(:user) }
        expect(response).to have_http_status(302)
      end
      it 'ユーザーが登録されること' do
        expect do
          post signup_path, params: { user: FactoryBot.attributes_for(:user) }
        end.to change(User, :count).by(1)
      end
      it 'リダイレクトすること' do
        post signup_path, params: { user: FactoryBot.attributes_for(:user) }
        expect(response).to redirect_to User.last
      end
    end

    context 'パラメータが不正な場合' do
      it 'リクエストが成功すること' do
        post signup_path, params: { user: FactoryBot.attributes_for(:user,name: '') }
        expect(response).to have_http_status(200)
      end

      it 'ユーザーが登録されないこと' do
        expect do
          post signup_path, params: { user: FactoryBot.attributes_for(:user,name: '') }
        end.to_not change(User, :count)
      end

      it 'エラーが表示されること' do
        post signup_path, params: { user: FactoryBot.attributes_for(:user,name: '') }
        expect(response.body).to include "error"
      end
    end
  end
end

```
③ログイン・ログアウトに関してのRequestSpec

```ruby:spec/requests/users_login_apis_spec.rb
require 'rails_helper'

RSpec.describe "ログイン・ログアウトに関して", type: :request do
  describe "GET #show" do
    include UsersHelper
    before do
      @user=FactoryBot.create(:user)
    end
    describe '正常にログイン＆ログアウト' do
      it "ログイン成功" do
        sign_in_as @user
        get user_path(@user)
        expect(response).to be_success
        expect(response).to have_http_status(200)
      end
      it 'ログアウト' do
        # 一旦ログイン
        sign_in_as @user
        get user_path(@user)
        expect(response).to be_success
        # ログアウト
        delete logout_path
        #be_falsey → nilか空白であればfalseです
        expect(response).to have_http_status(302)
        expect(session[:user_id]).to be_falsey # => nil
      end
    end

    context "ログインに失敗した時" do
      it "フラッシュメッセージの残留をキャッチすること" do
        get login_path
        expect(response).to have_http_status(:success)

        #「email:""」と「password:""」の値を持ってlogin_pathにアクセス
        # → バリデーションに引っかかりログインできない
        post login_path, params: { session: { email: "", password: "" }}
        expect(response).to have_http_status(:success)
        #flash[:danger]が表示されているかチェック
        expect(flash[:danger]).to be_truthy

        #root_path（別ページ）に移動してflash[:danger]が表示されていないかチェック
        get root_path
        expect(flash[:danger]).to be_falsey
      end
    end
  end
end
```
④Modelに関してのRspec

- 存在性
- 長さ
- フォーマット
- 一意性
- パスワードの確認

と項目を分けていることに注意。

```ruby:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  before do
    @user = FactoryBot.create(:user)
  end
  describe '存在性' do
    it '@userが有効であることの確認' do
      expect(@user).to be_valid
    end
    it 'nameが空白は無効' do
      @user.name = "     "
      expect(@user).to be_invalid
    end
    it "emailが空白は無効" do
      @user.email = "     "
      expect(@user).to be_invalid
    end
    it "password空白は無効" do
      @user.password = @user.password_confirmation = " " * 6
      expect(@user).to be_invalid
    end
  end

  describe '長さ' do
    it "name51文字以上は無効" do
      @user.name = "a" * 51
      expect(@user).to be_invalid
    end
    it "name50文字以下は有効" do
      @user.name = "a" * 50
      expect(@user).to be_valid
    end
    it "emailが256字以上は無効" do
      @user.email = "a" * 244 + "@example.com"
      expect(@user).to be_invalid
    end
    it "emailが255字以下は有効" do
      @user.email = "a" * 243 + "@example.com"
      expect(@user).to be_valid
    end
    it "password5字以下は無効" do
      @user.password = @user.password_confirmation = "a" * 5
      expect(@user).to be_invalid
    end
    it "password 6字以上は有効" do
      @user.password = @user.password_confirmation = "a" * 6
      expect(@user).to be_valid
    end
  end

  describe 'フォーマット' do
    it "emailは規定の表記でなければ無効" do
      invalid_addresses = %w[user@example,com foo@bar..com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com]
      invalid_addresses.each do |invalid_address|
        @user.email = invalid_address
        expect(@user).to be_invalid,"#{invalid_address.inspect}が無効ではありません"
      end
    end
    it "emailは規定の表記であれば有効" do
      valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn]
      valid_addresses.each do |valid_address|
      @user.email = valid_address
      expect(@user).to be_valid, "#{valid_address.inspect} が有効ではありません"
      end
    end
  end

  describe '一意性' do
    it "emailの重複は無効" do
      duplicate_user = @user.dup
      duplicate_user.email = @user.email.upcase
      @user.save!
      expect(duplicate_user).to be_invalid
    end
  end

  it "emailを小文字に変換後の値と大文字を混ぜて登録されたアドレスが同じか" do
    #わかりやすくベタ書き
    @user.email = "Foo@ExAMPle.CoM"
    @user.save!
    #全て小文字のemailと等しいかのテスト
    expect(@user.reload.email).to eq "foo@example.com"
  end

  describe 'passwordとpassward_comfirmの機能チェック' do
    it "一致する場合" do
      expect(@user).to be_valid
    end
    it "一致しない場合" do
      user = User.new(name: 'false',email: 'false@gmail.com', password: "password", password_confirmation: "different")
      user.valid?
      expect(user.errors[:password_confirmation]).to include("doesn't match Password")
    end
  end
end

```
以上でRailsチュートリアルの第3章〜第8章のMinitestをRspecでの書き換えを網羅できたと思われる。

#FactoryBotのcreateとbuildについて
Rspecでの作成の際にFactoryBotでcreateを使うべきか、buildを使うべきか迷う場面があった。

○　build
・DB上にはデータがないので、DBにアクセスする必要があるテストのときは使えない。
・DBにアクセスする必要がないので処理が比較的軽くなる。

○ create
・DB上にデータを作成する。
・DBにアクセスする処理のときは必須。

今回はDB上のデータを使用するためcreateを用いた。
#FactoryBot.attributes_forに関して※1
こちらはじめは次のように実装していた。

```
 post signup_path, params: params: { name:'test',email: 'test@gmail.com', password: 'aaaaaa', password_confirmation: 'aaaaaa' }
```

すると次のようなエラーになってしまう。

```
Failure/Error: params.require(:user).permit(:name, :email, :password,:password_confirmation)
     
ActionController::ParameterMissing:
param is missing or the value is empty: user
```
このエラーについて色々調べていく中でattributes_forメソッドを使った書き方にいきついた。
ですが、これを用いずに最初に書こうとしたやり方で`param is missing`にならない正しい書き方も知りたいので、助言いただけると助かります。
#まとめ
RequestSpecでの実装はMinitestのassertと一対一で書き換えられるものではないため、それに相当する書き方を模索するのに苦労した。
これに慣れるためには、Rspecのマッチャの学習をより進めていくことが近道なのだろう。

RequestSpecを用いるか、それともFeatureSpecを用いるかどちらの方がよいのだろうかと長時間悩んでいたが、実際はどちらでも可能というのが結論であった。
ただ、Railsチュートリアルの統合テストに関してはRequestSpecでの書き換えのほうが感覚的は容易であると書かれている。※2

#参考

※2
Junichi Ito
「【動画付き】Railsチュートリアルの統合テスト(integration test)は、RSpecのリクエストスペックに置き換えるのがラクです」より
https://qiita.com/jnchito/items/8d8d579cdca131c0db14