Railsチュートリアル テストをRSpecで実施 【フィーチャスペック編 authorization_spec】

<!--
# Railsチュートリアル テストをRSpecでやってみる 【フィーチャースペック編 authorization_spec】
-->
<!-- ## フィーチャースペック編 authorization_spec -->

### テスト対象

該当するコントローラ  | 該当するアクション
:--|:--
UsersController  |  index, create, edit, update, destroy, following, followers
MicropostsController  |  create, destroy

### 環境
[Userモデル 単体テスト編 1/3](https://qiita.com/shu130/items/57233462258d42566f18) に記載

#### フォルダ、使用ファイル

種類  |  ファイル名
:--|:--
スペック  |  spec/features/authorization_spec.rb
サポートモジュール  |  spec/support/support_module.rb
shared_context  |  spec/support/shared_context.rb
shared_examples  |  spec/support/shared_examples.rb
ファクトリ（ユーザ）  |  spec/support/factories/users.rb
ファクトリ（マイクロポスト）  |  spec/support/factories/microposts.rb


### 未ログインの場合（ページ保護）のスペック

### アウトラインの作成

```ruby:authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do
  # アクセス権限
  describe "in UsersController"
    # 未ログインの場合 （before_action のテスト）
    describe "login is necessary"
      context "when non-login"
        describe "index"
          # エラーメッセージ、ログインページにリダイレクト
        describe "edit"
          # エラーメッセージ、ログインページにリダイレクト
        describe "update"
          # エラーメッセージ、ログインページにリダイレクト
        describe "destroy"
          # エラーメッセージ、ログインページにリダイレクト
        describe "following"
          # エラーメッセージ、ログインページにリダイレクト
        describe "followers"
          # エラーメッセージ、ログインページにリダイレクト
    # ユーザ削除権限
    describe "authorization of delete user"
      # adminユーザの場合
      context "admin"
        # ユーザの削除ができること
        # 成功メッセージ
      # 一般ユーザの場合
      context "non-admin"
        # ユーザの削除ができないこと
        # ルートにリダイレクトされること

  describe "in MicropostsController"
    # マイクロポスト 投稿（作成）／削除
    describe "authorization of create/destroy micropost"
      context "when non-login"
        describe "create"
          # エラーメッセージ、ログインページにリダイレクト
        describe "destroy"
          # エラーメッセージ、ログインページにリダイレクト
end
```

#### shared_examples の作成 1/2

- ログインメッセージ出力・ログインページへリダイレクトは、一般化して使い回せるようにしてみる

- spec側でブロックをオブジェクト化(`Proc.new { }`)して `subject { }` に定義し、shared_examples側で、`subject.call` して呼び出す

```ruby:shared_examples.rb
# spec/support/shared_examples.rb

  # 成功メッセージ
  # flash[:success]
  shared_examples_for "success message" do |msg|
    it { subject.call; expect(flash[:success]).to eq msg }
  end

  # 失敗メッセージ
  # flash[:danger]
  shared_examples_for "error message" do |msg|
    it { subject.call; expect(flash[:danger]).to eq msg }
  end

  # リダイレクト
  # redirect to path
  shared_examples_for "redirect to path" do |path|
    it { subject.call; expect(response).to redirect_to path }
  end
```

#### shared_examples の作成 2/2

↓のように、ユーザの削除（admin権限）成功／失敗のスペックを作成

- `it_behaves_like "success delete user"`
- `it_behaves_like "fail delete user"`

```ruby:shared_examples.rb
# spec/support/shared_examples.rb

  # ユーザの削除（admin権限）
  # users#destroy

  # 成功
  shared_examples_for "success delete user" do
    scenario "user decrememt -1" do
      login_as(admin)
      click_link "Users"
      expect(page).to have_current_path("/users")
      expect(page).to have_link('delete', href: user_path(User.first))
      expect(page).to have_link('delete', href: user_path(User.second))
      expect(page).not_to have_link('delete', href: user_path(admin))
      expect {
        click_link('delete', match: :first)
        # 成功メッセージ
        expect(page).to have_css("div.alert.alert-success", text: "User deleted")
      }.to change(User, :count).by(-1)
    end
  end

  # 失敗
  shared_examples_for "fail delete user" do
    scenario "user decrement 0" do
      login_as(user)
      click_link "Users"
      expect(page).to have_current_path("/users")
      expect(page).not_to have_link('delete', href: user_path(User.first))
      expect(page).not_to have_link('delete', href: user_path(User.second))
      expect {
        # リンクが無いので、直接 HTTPリクエストを発行
        delete user_path(User.first)
      }.to change(User, :count).by(0)
    end
  end

```

### スペック作成

```ruby:authorization_spec.rb
# spec/features/authorization_spec.rb
require 'rails_helper'

RSpec.feature "Authorization", type: :feature do

  include SupportModule
  include_context "setup"

  # アクセス権限
  describe "in UsersController", type: :request do
    describe "login is necessary" do
      context "when non-login" do
        describe "index" do
          subject { Proc.new { get users_path } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "edit" do
          subject { Proc.new { get edit_user_path(user) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "update" do
          subject { Proc.new { patch user_path(user), params: { user: update_params_1 } } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "destroy" do
          subject { Proc.new { delete user_path(other_user) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "following" do
          subject { Proc.new { get following_user_path(user) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "followers" do
          subject { Proc.new { get followers_user_path(user) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
      end
    end
    # ユーザ削除権限
    describe "authorization of delete user" do
      before { users }
      it { expect(User.count).to eq users.count }
      # adminユーザの場合
      context "admin" do
        it_behaves_like "success delete user"
      end
      # 一般ユーザの場合
      context "non-admin" do
        it_behaves_like "fail delete user"
        it { expect(page).to have_current_path("/") }
      end
    end
  end

  describe "in MicropostsController", type: :request do
    # マイクロポスト 投稿（作成）／削除
    describe "authorization of create/destroy micropost" do
      context "when non-login" do
        describe "create" do
          subject { Proc.new { post microposts_path, params: post_params } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "destroy" do
          subject { Proc.new { delete micropost_path(my_post.id) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
      end
    end
  end
end
```

#### SupportModule, shared_context, ファクトリ

- SupportModule

  ```ruby:support_module.rb
  # spec/support/support_module.rb
  module SupportModule
    def login_as(user)
      visit root_path
      click_link "Log in"
      fill_in "Email",    with: user.email
      fill_in "Password", with: user.password
      click_button "Log in"
    end
  end
  ```

- shared_context

  ```ruby:shared_context.rb
  # spec/support/shared_context.rb
  RSpec.shared_context "setup" do
    # ユーザ
    # 遅延評価、呼ばれた時にDB保存される
    let(:user) { create(:user) }
    let(:admin) { create(:admin) }

    # マイクロポスト
    # 自分の投稿
    let(:my_post) { create(:user_post) }
    # 属性をハッシュ化して呼ばれた時に使う
    let(:post_params) { attributes_for(:user_post) }
  end
  ```

- ファクトリ

  ```ruby:users.rb
  # spec/factories/users.rb
  FactoryBot.define do
    # 自分
    # factory [任意のファクトリ名], class: [クラス名]
    factory :user, class: User do
      name     "Example user"
      email    "user@example.com"
      password              "foobar"
      password_confirmation "foobar"
      admin false
      # 他人
      factory :other_user do # => 入れ子にしているので[クラス名]は不要
        name { Faker::Name.name }
        email { Faker::Internet.email }
        # 管理者ユーザ
        factory :admin do
          admin true
        end
      end
    end
  end
  ```
  ```ruby:microposts.rb
  # spec/factories/microposts.rb
  FactoryBot.define do
    # 自分のマイクロポスト
    factory :user_post, class: Micropost do
      content { Faker::Lorem.sentence(5) }
      association :user, factory: :user
      # 他人のマイクロポスト
      factory :other_user_post do
        content { Faker::Lorem.sentence(5) }
        association :user, factory: :other_user
      end
    end
  end
  ```

### 実行結果

```test
$ bin/rspec spec/features/authorization_spec.rb
Running via Spring preloader in process 17859

Authorization
  in UsersController
    login is necessary
      when non-login
        index
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        edit
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        update
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        destroy
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        following
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        followers
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
    authorization of delete user
      should eq 30
      admin
        behaves like success delete user
          user decrememt -1
      non-admin
        should have current path "/"
        behaves like fail delete user
          user decrement 0
  in MicropostsController
    authorization of create/destroy micropost
      when non-login
        create
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        destroy
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"

Finished in 13.41 seconds (files took 2.92 seconds to load)
20 examples, 0 failures

```
--------------------------------

### 参考

- [Railsチュートリアル Rails4.0(第2版)](https://railstutorial.jp/chapters/modeling-users?version=4.0#top)

- [(書籍)leanpub - Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp/)

--------------------------------

### 以上