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

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

### テスト対象

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

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

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

種類  |  ファイル名
:--|:--
スペック  |  spec/features/users_profile_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
ファクトリ（マイクロポスト）  |  spec/support/factories/microposts.rb


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

- ページタイトル、パス、リンク

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

```ruby:users_profile_spec.rb
spec/features/users_profile_spec.rb

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

  describe "profile"
    # ページタイトルと見出しが正しいこと
    scenario "have title/heading"
    # パスが正しいこと
    scenario "have correct path"
    # 存在すべきリンクが表示されること
    scenario "have links of profile-page"

end
```

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

##### ↓の部分は、shared_examples（別ファイル）に作成

- `scenario "have links of profile-page"`

```ruby:users_profile_spec.rb
spec/features/users_profile_spec.rb

  include SupportModule
  include_context "setup"

  subject { page }

  describe "profile" do
    before { login_as(user) }
    # => SupportModule, shared_context 内で定義
    scenario "have title/heading" do
      should have_title(user.name)
      should have_css("h1", text: user.name)
    end
    # パスが正しいこと
    scenario "have correct path" do
      should have_current_path(user_path(user)) # or
      expect(current_path).to eq user_path(user)
    end
    # 存在すべきリンクが表示されること
    it_behaves_like "have links of profile-page"
  end
end
```

#### SupportModule, shared_context

- `before { login_as(user) }` の部分

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

    # 遅延評価、呼ばれた時にDB保存される
    let(:user) { create(:user) }

  end
  ```
  ```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_examples の作成

- `it_behaves_like "have links of profile-page"` の部分

  ```ruby:shared_examples.rb
  # spec/support/shared_examples.rb
    # links
    # users#show
    shared_examples_for "have links of profile-page" do
      it { expect(page).to have_link("Users",    href: "/users") }
      it { expect(page).to have_link("Profile",  href: user_path(user)) }
      it { expect(page).to have_link("Settings", href: edit_user_path(user)) }
      it { expect(page).to have_link("Log out",  href: "/logout") }
      it { expect(page).not_to have_link("Log in",  href: "/login") }
    end
  ```

### 実行結果 1/4

```test
$ bin/rspec spec/features/users_profile_spec.rb -e "profile"

UsersProfile
  profile
    have title/heading
    have correct path
    behaves like have links of profile-page
      should have visible link "Users" with href "/users"
      should have visible link "Profile" with href "/users/5"
      should have visible link "Settings" with href "/users/5/edit"
      should have visible link "Log out" with href "/logout"
      should not have visible link "Log in" with href "/login"

Finished in 5.45 seconds (files took 2.17 seconds to load)
7 examples, 0 failures

```

<!-- 1/4 -->
--------------------------------



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

```ruby:users_profile_spec.rb
# spec/features/users_profile_spec.rb
RSpec.feature "UsersProfile", type: :feature do

  describe "profile"

    # （省略）

    # ユーザー情報
    describe "user-info"
      # ユーザー情報が表示されていること
      scenario "have user profile infomation"
    # マイクロポストの統計
    describe "micropost stats"
      # フォロー数／フォロワー数のリンクと数字が表示されていること
      scenario "have link user's following/followers"

end
```

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

↓の部分は shared_examples（別ファイル）に作成

- `scenario "have user profile infomation"`
- `scenario "have link user's following/followers"`

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

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

  include SupportModule
  include_context "setup"

  subject { page }

  describe "profile" do
    before { login_as(user) } # => SupportModule

    # （省略）

    # ユーザー情報
    describe "user-info" do
      before { my_posts }
      # （セットアップの確認）
      it { expect(user.microposts.count).to eq my_posts.count }
      it_behaves_like "have user profile infomation"
    end
    # マイクロポストの統計
    describe "micropost stats" do
      before do
        my_posts
        other_user
        other_posts
        user.follow(other_user)
        other_user.follow(user)
      end
      # （セットアップの確認）
      it { expect(user.microposts.count).to eq my_posts.count }
      it { expect(other_user.microposts.count).to eq other_posts.count }
      it_behaves_like "have link user's following/followers"
    end
  end
end
```

#### shared_context の作成

- ユーザ、マイクロポストのセットアップの定義

  ```ruby:shared_context.rb
  spec/support/shared_context.rb

  RSpec.shared_context "setup" do

    # ユーザ
    # 遅延評価、呼ばれた時にDB保存される
    let(:user) { create(:user) }
    let(:other_user) { create(:other_user) }

    # マイクロポスト
    # 自分の投稿
    let(:my_posts) { create_list(:user_post, 30, user: user) }
    # 他人の投稿
    let(:other_posts) { create_list(:other_user_post, 30, user: other_user) }

  end
  ```

#### サンプルデータ has_many な関係を定義する

`let(:my_posts) { create_list(:user_post, 30) }` ではなく、
`let(:my_posts) { create_list(:user_post, 30, user: user) }` とする

- 同じファクトリ(user)を作成しようとして、email 重複エラーにならないように、インスタンス変数(user)を明示的に渡して１対多になるようにする

  ```test
  （抜粋）email 重複エラーになる

  $ rails console test --sandbox

  [2] pry(main)> user = FactoryBot.create(:user)
   id: 5,
   name: "Example user",
   email: "user@example.com",
   created_at: Fri, 03 Aug 2018 02:10:18 UTC +00:00,
   updated_at: Fri, 03 Aug 2018 02:10:18 UTC +00:00,
   password_digest: "$2a$04$TB5uZ0B1Zti3Le1a3YV7EOcE72HxAT/GfLoiuU2s23b4IBfFjeiNK",
   remember_digest: nil,
   admin: false,
   activation_digest: "$2a$04$fkkSn6p5ZqaUGXNc0PAUB.HXf4LchtR35S0Fs6C4Bk0ujw5Sg1igq",
   activated: false,
   activated_at: nil>

  [6] pry(main)> mypost = FactoryBot.create_list(:user_post, 1)
     (0.2ms)  SAVEPOINT active_record_1
    User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "user@example.com"], ["LIMIT", 1]]
     (0.1ms)  ROLLBACK TO SAVEPOINT active_record_1
  ActiveRecord::RecordInvalid: Validation failed: Email has already been taken
  from /home/vagrant/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.1.4/lib/active_record/validations.rb:78:in `raise_validation_error'
  ```

#### ファクトリの作成

- ユーザ／マイクロポスト

  ```ruby:users.rb
  # spec/factories/users.rb
  FactoryBot.define do
    # 自分
    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
  ```
  ```ruby:microposts.rb
  # spec/factories/micoroposts.rb
  FactoryBot.define do

    # 自分のマイクロポスト
    factory :user_post, class: Micropost do
      content { Faker::Lorem.sentence(5) }
      # association :[関連するモデル名], factory: :[任意につけたファクトリ名]
      association :user, factory: :user

      # 他人のマイクロポスト
      factory :other_user_post do
        content { Faker::Lorem.sentence(5) }
        association :user, factory: :other_user
      end
    end
  end
  ```

#### shared_examples の作成

- `it_behaves_like "have user profile infomation"`
- `it_behaves_like "have link user's following/followers"`

  ```ruby:shared_examples.rb
  # spec/support/shared_examples.rb
    # ユーザー情報
    # top#home, users#following, users#followers
    shared_examples_for "have user infomation" do
      it { expect(page).to have_css('img.gravatar') }
      it { expect(page).to have_css('h1', text: user.name) }
      it { expect(page).to have_text("Microposts") }
      it { expect(page).to have_link("view my profile", href: user_path(user)) }
    end

    # マイクロポストの統計情報
    # tops#home, users#show
    shared_examples_for "have link user's following/followers" do
      it { expect(page).to have_link("following", href: following_user_path(user)) }
      it { expect(page).to have_link("followers", href: followers_user_path(user)) }
    end
  ```

### 実行結果 2/4

```test
$ bin/rspec spec/features/users_profile_spec.rb -e "micropost stats"

UsersProfile
  profile
    micropost stats
      should eq 30
      should eq 30
      behaves like have link user's following/followers
        should have visible link "following" with href "/users/5/following"
        should have visible link "followers" with href "/users/5/followers"

Finished in 6.51 seconds (files took 2.48 seconds to load)
4 examples, 0 failures

```


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




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

```ruby:users_profile_spec.rb
spec/features/users_profile_spec.rb

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

  describe "profile"

    # （省略）

    # マイクロポスト
    describe "microposts"
      # マイクロポストが表示されていること
      scenario "pagination list each micropost"

end
```

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

```ruby:users_profile_spec.rb
# spec/features/users_profile_spec.rb
RSpec.feature "UsersProfile", type: :feature do

  describe "profile"
    before { login_as(user) }

    # （省略）

    # マイクロポスト
    describe "microposts" do
      before { my_posts }
      # （セットアップの確認）
      it { expect(user.microposts.count).to eq my_posts.count }

      # マイクロポストが表示されていること
      scenario "pagination list each micropost" do
        # ？？？ＮＧになってしまう
        # should have_css("h3", text: "Micropost (#{user.microposts.count})")
        expect {
          user.microposts.paginate(page: 1).each do |post|
            should have_link("img.gravatar", href: post.user)
            should have_link("#{micropost.user.name}", href: post.user)
            should have_css("li#micropost-#{post.id}", text: post.content)
          end
        }
      end
    end
  end
end
```

### 実行結果 3/4

```test
$ bin/rspec spec/features/users_profile_spec.rb -e "micropost"

UsersProfile
  profile
    microposts
      should eq 30
      pagination list each micropost

Finished in 4.77 seconds (files took 1.91 seconds to load)
2 examples, 0 failures

```


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


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

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

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

  describe "profile"

    # （省略）

    # フォロー／フォロー解除 ボタン
    describe "follow/unfollow buttons"
      # フォロー
      context "following other-user"
        # （自分にとって）following が 1 増加すること (increment: 1)
        scenario "following increment 1 (for user)"
        # （相手にとって）follower が 1 増加すること (increment: 1)
        scenario "followers increment 1 (for other-user)"
        # ボタンが Unfollow に変わる
        scenario "toggle 'Unfollow'"
      # フォロー解除
      context "unfollow other-user"
        # （自分にとって）following が 1 減少すること (decrement: -1)
        scenario "following decrement -1 (for user)"
        # （相手にとって）follower が 1 減少すること (decrement: -1)
        scenario "followers decrement -1 (for other-user)"
        # ボタンが Follow に変わる
        scenario "toggle 'Follow'"
end
```

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

```ruby:users_profile_spec.rb
# spec/features/users_profile_spec.rb
RSpec.feature "UsersProfile", type: :feature do

  describe "profile"
    before { login_as(user) }

    # （省略）

    # フォロー／フォロー解除 ボタン
    describe "follow/unfollow buttons" do
      # フォローする相手のパスに移動
      before { visit user_path(other_user) }
      # フォロー
      context "following other-user" do
        subject { click_button "Follow" }
        # （自分にとって）following が 1 増加すること (increment: 1)
        scenario "following increment 1 (for user)" do
          expect { subject }.to change(user.following, :count).by(1)
        end
        # （相手にとって）follower が 1 増加すること (increment: 1)
        scenario "followers increment 1 (for other-user)" do
          expect { subject }.to change(other_user.followers, :count).by(1)
        end
        # ボタンが Unfollow に変わる
        scenario "toggle 'Unfollow'" do
          expect {
            subject
            expect(page).to have_css "div#follow_form", text: "Unfollow"
          }
        end
      end
      # フォロー解除
      context "unfollow other-user" do
        before { click_button "Follow" }
        subject { click_button "Unfollow" }
        scenario "following decrement -1 (for user)" do
          expect { subject }.to change(user.following, :count).by(-1)
        end
        scenario "followers decrement -1 (for other-user)" do
          expect { subject }.to change(other_user.followers, :count).by(-1)
        end
        scenario "toggle 'Follow'" do
          expect {
            subject
            expect(page).to have_css("div#follow_form", text: "Follow")
          }
        end
      end
    end
  end
end
```


### 実行結果 4/4

```test
$ bin/rspec spec/features/users_profile_spec.rb -e "follow/unfollow buttons"

UsersProfile
  profile
    follow/unfollow buttons
      following other-user
        following increment 1 (for user)
        followers increment 1 (for other-user)
        toggle 'Unfollow'
      unfollow other-user
        following decrement -1 (for user)
        followers decrement -1 (for other-user)
        toggle 'Follow'

Finished in 5.57 seconds (files took 1.94 seconds to load)
6 examples, 0 failures

```


<!-- 4/4 -->
--------------------------------

### 参考

- [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_edit_spec 5/7](https://qiita.com/shu130/items/c50f62bb81294da7b85c)