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

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

### テスト対象

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

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

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

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

- ユーザ情報の変更（更新）

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

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

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

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

  describe "edit"
    # 情報が valid の場合
    context "valid info"
      scenario "success edit profile"
    # 情報が invalid の場合
    context "invalid info"
      scenario "fail edit profile"

end
```

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

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

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

  include SupportModule
  include_context "setup"

  subject { page }

  describe "edit" do
    # 情報が valid の場合
    context "valid info" do
      scenario "success edit profile" do
        login_as(user)
        click_link "Settings"
        should have_title("Edit user")
        should have_css("h1", text: "Update your profile")
        should have_link("change", href: "http://gravatar.com/emails")
        expect {
          fill_in_update_profile_form("New Name", "new@example.com")
          click_button "Save changes"
        }.to change(User, :count).by(0)
        expect(user.reload.name).to eq "New Name"
        expect(user.reload.email).to eq "new@example.com"
        success_messages("Profile updated")
      end
    end
    # 情報が invalid の場合
    context "invalid info" do
      scenario "fail edit profile" do
        login_as(user)
        click_link "Settings"
        expect {
          fill_in_update_profile_form("", "foo@")
          click_button "Save changes"
        }.to change(User, :count).by(0)
        expect(user.reload.name).not_to eq ""
        expect(user.reload.email).not_to eq "foo@"
        error_messages("errors")
      end
    end
  end
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
    end
  end
  ```

#### SupportModule の作成

↓の部分をメソッドとして定義

- `fill_in_update_profile_form`
- `success_messages("Profile updated")`
- `error_messages("errors")`

  ```ruby:support_module.rb
  # spec/support/support_module.rb
  module SupportModule

    def fill_in_update_profile_form(name, email, password = "", confirmation = "")
      fill_in "Name",         with: name
      fill_in "Email",        with: email
      fill_in "Password",     with: password
      fill_in "Confirmation", with: confirmation
    end

    def success_messages(msg)
      expect(page).to have_css("div.alert.alert-success", text: msg)
    end

    def error_messages(msg = "")
      if msg.empty?
        should have_css('div.alert.alert-danger')
      else
        should have_css('div.alert.alert-danger', text: msg)
      end
    end
  end
  ```

### 実行結果 1/2

```test
$ bin/rspec spec/features/users_edit_spec.rb -e "info"

UsersEdit
  edit
    valid info
      success edit profile
    invalid info
      fail edit profile

Finished in 4.96 seconds (files took 2.3 seconds to load)
2 examples, 0 failures

```


<!-- 1/2 -->
--------------------------------




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

```ruby:users_edit_spec.rb
# spec/features/users_edit_spec.rb
RSpec.feature "UsersEdit", type: :feature do

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

  describe "edit"

    # （省略）

    # admin属性
    describe "admin-attribute"
      # admin属性をweb経由で変更できないこと
      scenario "not allow change via the web"
    # フレンドリーフォワーディング
    describe "friendly forwarding"
      # ログイン後は、
      context "after login (non-login-user)"
        # 目的としていたページに遷移していること
        scenario "render desired page"
end
```

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

- `patch user_path(user), params: { user: admin_params }` の部分は、直接HTTPリクエストを送るので、オプション `type: :request` をつけないと `NoMethodError` になってしまう

```ruby:users_edit_spec.rb
# spec/features/users_edit_spec.rb
RSpec.feature "UsersEdit", type: :feature do

  describe "edit"

    # （省略）

    # admin属性
    describe "admin-attribute", type: :request do
      # web経由で変更できないこと
      scenario "not allow change via the web" do
        patch user_path(user), params: { user: admin_params }
        expect(user.reload).not_to be_admin
        should have_current_path("/")
      end
    end

    # フレンドリーフォワーディング
    describe "friendly forwarding" do
      # ログイン後は、
      context "after login (non-login-user)" do
        # 目的としていたページに遷移していること
        scenario "render desired page" do
          visit edit_user_path(user)
          error_messages("Please log in")
          should have_current_path("/login")
          login_as(user)
          should have_current_path(edit_user_path(user))
          should have_title("Edit user")
          should have_css("h1", text: "Update your profile")
        end
      end
    end
  end
end
```

#### shared_context の作成

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

    # 属性をハッシュ化して呼ばれた時に使う
    let(:admin_params) { attributes_for(:user, admin: true) }

  end
  ```

#### ファクトリの使い方

##### `FactoryBot.attributes_for(:user, admin: true)`

- 属性値をハッシュ化する

  ```test
  $ rails console test --sandbox

  [1] pry(main)>
  [2] pry(main)> params = FactoryBot.attributes_for(:user, admin: true)
  => {:name=>"Example user",
   :email=>"user@example.com",
   :password=>"foobar",
   :password_confirmation=>"foobar",
   :admin=>true}
  ```


### 実行結果 2/2

```test
$ bin/rspec spec/features/users_edit_spec.rb -e "admin-attribute"

UsersEdit
  edit
    admin-attribute
      not allow change via the web

Finished in 1.58 seconds (files took 2.02 seconds to load)
1 example, 0 failures

```
```test
$ bin/rspec spec/features/users_edit_spec.rb -e "friendly forwarding"

UsersEdit
  edit
    friendly forwarding
      after login (non-login-user)
        render desired page

Finished in 3.96 seconds (files took 2.09 seconds to load)
1 example, 0 failures

```


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




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

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

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

```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 "edit"
          # ログインメッセージが出力されること
          it "should have error_messages 'Please log in'"
          # ログインページへリダイレクトされること
          it "should have redirect to '/login'"
        describe "update"
          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_expamples.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
  ```

#### スペック作成
<!--
#### 未ログインの場合（ページ保護）のスペックは authorization_spec.rb （別ファイル）にて作成
 -->
```ruby:authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do

  include SupportModule
  include_context "setup"

  # アクセス権限
  describe "in UsersController", type: :request do
    # 未ログインの場合 （before_action のテスト）
    describe "login is necessary" do
      context "when non-login" do
        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
      end
    end
  end
end
```

#### 実行結果

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

Authorization
  in UsersController
    login is necessary
      when non-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"

Finished in 2.11 seconds (files took 2.38 seconds to load)
4 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/)

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

### 続く
[フィーチャスペック編 users_follow_spec 6/7](https://qiita.com/shu130/items/78101382f4eb6cc4f7ae)