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

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

### テスト対象

該当するコントローラ  | 該当するアクション
:--|:--
UsersController  |  index

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

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

種類  |  ファイル名
:--|:--
スペック  |  spec/features/users_index_spec.rb
スペック  |  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


### アウトラインの作成 1/3

- ページネーション

- ※ 未ログインの場合（ページ保護）／ユーザ削除権限（admin権限）のテストは `authorization_spec.rb` （別ファイル）にて作成
  [フィーチャスペック編 authorization_spec](https://qiita.com/shu130/items/700379de730d34ef3849)

```ruby:users_index_spec.rb
# spec/features/users_index_spec.rb
RSpec.feature "UsersIndex", type: :feature do

  # 未ログイン場合のスペックは、
  # authorization_spec.rb にて（別ファイル）作成

  describe "index"
    # ページネーション
    describe "pagination"
      # ページネーションでユーザが表示されること
      scenario "list each user"

end
```

### スペック作成 1/3

```ruby:users_index_spec.rb
# spec/features/users_index_spec.rb

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

  include SupportModule
  include_context "setup"

  subject { page }

  describe "index" do
    # ページネーション
    describe "pagination" do
      # （ファクトリをセット）
      before { users }  # => shared_context 内で定義
      # （セットアップの確認）
      it { expect(User.count).to eq users.count }
      # ページネーションでユーザが表示されること
      scenario "list each user" do
        login_as(user)  # => SupportModule 内で定義
        click_link "Users"
        should have_current_path("/users")
        should have_title("All users")
        should have_css("h1", text: "All users")
        User.paginate(page: 1).each do |user|
          expect(page).to have_css("li", text: user.name)
        end
      end
    end
  end
end

```

#### shared_context "setup"の作成、ファクトリの作成

- `before { users }` の部分

  ```ruby:shared_context.rb
  # spec/support/shared_context.rb
  RSpec.shared_context "setup" do

    # 遅延評価、呼ばれた時にDB保存される
    let(:users) { create_list(:other_user, 30) }

  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 }
      end
    end
  end
  ```

#### SupportModule 作成

- `login_as(user)` の部分

  ```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
  ```

### 実行結果 1/3

```test
$ bin/rspec spec/features/users_index_spec.rb

UsersIndex
  index
    pagination
      should eq 30
      list each user

Finished in 5.22 seconds (files took 2.1 seconds to load)
2 examples, 0 failures

```

<!-- 1/3 -->
--------------------------------


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

- authorization_spec.rb （別ファイル）にて作成

#### アウトラインの作成 2/3

```ruby:authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do

  # ページ保護（アクセス権限）
  describe "in UsersController"
    describe "login is necessary"
      # 未ログインの場合
      context "when non-login"
        describe "index"
          # ログインメッセージが出力されること
          it "should have error_messages 'Please log in'"
          # ログインページへリダイレクトされること
          it "should have redirect to '/login'"
end
```

#### shared_examples の作成

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

- 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
  ```

### スペック作成 2/3

- spec側でブロックをオブジェクト化(`Proc.new { }`)して `subject { }` に定義
- `get users_path` の部分は、直接HTTPリクエストを送るので、オプション `type: :request` をつけないと `NoMethodError` になってしまう

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

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

  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
      end
    end
  end
end
```

### 実行結果 2/3

```test
$ bin/rspec spec/features/authorization_spec.rb -e "index"

Authorization
  in UsersController
    login is necessary
      when non-login
        index
          behaves like error message
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"

Finished in 1.64 seconds (files took 2.31 seconds to load)
2 examples, 0 failures
```

<!-- 2/3 -->
--------------------------------


### ユーザ削除権限（admin権限）のスペック

<!-- - ユーザ削除権限（admin権限）のスペックも authorization_spec.rb （別ファイル）にて作成 -->
-  authorization_spec.rb （別ファイル）にて作成

#### アウトライン作成 3/3

```ruby:authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do

  describe "in UsersController", type: :request do

    # （省略）

    # ユーザ削除権限
    describe "authorization of delete user"
      # adminユーザの場合
      context "admin"
        # ユーザの削除ができること
        it "success create user"
        # 成功メッセージ
        it "have success messages"
      # 一般ユーザの場合
      context "non-admin"
        # ユーザの削除ができないこと
        it "fail delete user"
        # ルートにリダイレクトしていること
        it "have current path '/'"
end
```

#### スペック作成 3/3

```ruby:authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do

  include SupportModule     # => login_as(admin) を使う
  include_context "setup"

  describe "in UsersController", type: :request do

    # （省略）

    # ユーザ削除権限
    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 { should have_current_path("/") }
      end
    end
  end
end
```

#### shared_examples の作成

- `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

  ```

#### 実行結果 3/3

```test
$ bin/rspec spec/features/authorization_spec.rb -e "authorization of delete user"

Authorization
  in UsersController
    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

Finished in 8.17 seconds (files took 2.23 seconds to load)
4 examples, 0 failures

```

<!-- 3/3 -->
--------------------------------

### 参考

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

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

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


### 続く
[フィーチャスペック編 users_signup_spec 2/7](https://qiita.com/shu130/items/393f4c3bb72b6823be61)