Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Railsチュートリアル 第14章 ユーザーをフォローする - [Follow] のWebインターフェイス

More than 1 year has passed since last update.

フォローのサンプルデータ

別記事で解説します。

演習 - フォローのサンプルデータ

1.コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。

「最初のユーザーをフォローしている人の数」ということですね。「4番目から41番めのユーザーの合計数」、すなわち(3..40).countの値と一致するはずです。

>> User.first.followers.count
=> 38

>> (3..40).count
=> 38

>> User.first.followers.count == (3..40).count
=> true

一致していますね。

2. 先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。

「最初のユーザーがフォローしている人の数」ということですね。「3番目から51番めのユーザーの合計数」、すなわち(2..50).countの値と一致するはずです。

>> User.first.following.count
=> 49

>> (2..50).count
=> 49

>> User.first.following.count == (2..50).count
=> true

一致していますね。

統計と [Follow] フォーム

別記事で解説します。

演習 - 統計と [Follow] フォーム

1.1. ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では [Unfollow] ボタンが表示されているはずです。

ログインユーザーのidが1で、DBの内容がフォローのサンプルデータの内容であるとすると、 /users/2 へのアクセス結果は以下の通りです。

スクリーンショット 2020-02-01 22.59.29.png

対応するRailsサーバーのログ(抜粋)は以下のようになります。

Started GET "/users/2" for 172.17.0.1 at 2020-02-01 13:55:21 +0000
  Rendering users/show.html.erb within layouts/application
  Rendered shared/_stats.html.erb (15.4ms)
  Rendered users/_follow.html.erb (3.2ms)
  Rendered users/_follow_form.html.erb (32.1ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (16.0ms)
  Rendered users/show.html.erb within layouts/application (148.5ms)

users/_follow.html.erb、ならびにusers/_follow_form.html.erbが描画されているのがわかります。

また、/users/5 へのアクセス結果は以下の通りです。

スクリーンショット 2020-02-01 22.59.35.png

対応するRailsサーバーのログ(抜粋)は以下のようになります。

Started GET "/users/5" for 172.17.0.1 at 2020-02-01 13:57:32 +0000
  Rendering users/show.html.erb within layouts/application
  Rendered shared/_stats.html.erb (10.7ms)
  Rendered users/_unfollow.html.erb (5.2ms)
  Rendered users/_follow_form.html.erb (31.5ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (27.0ms)
  Rendered users/show.html.erb within layouts/application (141.3ms)

users/_unfollow.html.erb、ならびにusers/_follow_form.html.erbが描画されているのがわかります。

1.2. /users/1 にアクセスすると、どのような結果が表示されるでしょうか?

ログインユーザーのidが1である場合、 /users/1 においては、[Follow]/[Unfollow]ボタンをレンダリングする場所そのものが確保されず、これらのボタンも表示されないはずです。どうなっているでしょうか。

スクリーンショット 2020-01-03 14.04.18.png

確かに想定通りの動作になっています。

対応するRailsサーバーのログ(抜粋)は以下のようになります。

Started GET "/users/1" for 172.17.0.1 at 2020-02-01 14:05:02 +0000
  Rendering users/show.html.erb within layouts/application
  Rendered shared/_stats.html.erb (12.5ms)
  Rendered users/_follow_form.html.erb (0.5ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (16.7ms)
  Rendered users/show.html.erb within layouts/application (132.9ms)

users/_follow.html.erbおよびusers/_unfollow.html.erbはいずれもレンダリングされず、users/_follow_form.html.erbのみがレンダリングされているのがわかります。

2. ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。

現在ログインしているユーザーは、id=1のユーザーであることを前提とします。前述の演習「演習 - フォローのサンプルデータ」より、以下の表示内容となることが期待されます。

  • Homeページ(/)とプロフィールページ(/users/1)の両方に統計情報パーシャルが表示される
  • 「following」の前に表示される数はUser.first.following.countと一致する
  • 「followers」の前に表示される数はUser.first.followers.countと一致する
>> User.first.following.count
=> 49

>> User.first.followers.count
=> 38

まずはHomeページの表示結果です。「49 following / 38 followers」という表示内容に問題はありません。

スクリーンショット 2020-02-02 19.32.31.png

このとき、Railsサーバーに記録されるログ(抜粋)は以下のようになります。

Started GET "/" for 172.17.0.1 at 2020-02-02 10:32:27 +0000
  Rendering static_pages/home.html.erb within layouts/application
  Rendered shared/_user_info.html.erb (7.1ms)
  Rendered shared/_stats.html.erb (10.7ms)
  Rendered shared/_error_messages.html.erb (0.5ms)
  Rendered shared/_micropost_form.html.erb (20.3ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (118.9ms)
  Rendered shared/_feed.html.erb (151.2ms)
  Rendered shared/_home_logged_in.erb (289.7ms)
  Rendered static_pages/home.html.erb within layouts/application (317.0ms)

統計情報パーシャルの実体であるshared/_stats.html.erbが描画されているのがわかりますね。

続いてプロフィールページの表示結果です。Homeページと同様、「49 following / 38 followers」という表示内容に問題はありません。

スクリーンショット 2020-02-02 19.32.25.png

このとき、Railsサーバーに記録されるログ(抜粋)は以下のようになります。

Started GET "/users/1" for 172.17.0.1 at 2020-02-02 10:31:33 +0000
  Rendering users/show.html.erb within layouts/application
  Rendered shared/_stats.html.erb (16.7ms)
  Rendered users/_follow_form.html.erb (0.5ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (13.5ms)
  Rendered users/show.html.erb within layouts/application (120.5ms)

こちらも、統計情報パーシャルの実体であるshared/_stats.html.erbが描画されている様子が記録されています。

3.1. Homeページに表示されている統計情報に対してテストを書いてみましょう。

ヒント: リスト 13.28で示したテストに追加してみてください。

テストコードの実体

統計情報に対するテストコードの実体は以下のようになります。

assert_select 'strong', { id: 'following', text: /#{@user.following.count.to_s}/ }
assert_select 'strong', { id: 'followers', text: /#{@user.followers.count.to_s}/ }

上記コードは、以下の事柄についてテストを行っています。

  • CSS idがfollowingであり、テキストとしてログイン済みユーザーがフォローしているユーザーの数を含むstrong要素が描画されていること
  • CSS idがfollowersであり、テキストとしてログイン済みユーザーのフォロワーの数を含むstrong要素が描画されていること

Homeページに対するテストをどこに書くか

Homeページに対するテストの実装は、test/integration/site_layout_test.rbに既に存在します。しかしながら、test/integration/site_layout_test.rb上のテストというのは、header要素内やfooter要素内といった「サイト全体に適用されるレイアウトに関するテスト」と考えるべき趣旨のものです。コンテンツ内容に関するテストを含めるのは適当ではないと考えます。

というわけで、新たに統合テストを生成してしまいましょう。「Homeページに対するテスト」ということで、統合テストの名前はhomeとします。

# rails generate integration_test home
      invoke  test_unit
      create    test/integration/home_test.rb

実際のテストの記述

実際にHomeページに対するテストを記述していきます。記述場所は、只今生成したばかりのtest/integration/home_test.rbです。

test/integration/home_test.rb
require 'test_helper'

class HomeTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:rhakurei)
    log_in_as(@user)
  end

  test "home should include following and followers with login" do
    get root_path
    assert_select 'strong', { id: 'following', text: /#{@user.following.count.to_s}/ }
    assert_select 'strong', { id: 'followers', text: /#{@user.followers.count.to_s}/ }
  end
end

Homeページ上の統計情報の描画に対するテストが成功することを確認する

現時点で、上記のテストは問題なく成功します。

# rails test test/integration/home_test.rb
Running via Spring preloader in process 754
Started with run options --seed 26350

  1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.34328s
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

Homeページ上の統計情報の描画に対するテストが失敗する例

例えば、app/views/shared/_home_logged_in.erbに以下の欠落がある場合を考えてみます。

app/views/shared/_home_logged_in.erb
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="stats">
-       <%= render 'shared/stats' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>

この状態でtest/integration/home_test.rbに対するテストを行うと、以下のようにテストが失敗します。

# rails test test/integration/home_test.rb
Running via Spring preloader in process 767
Started with run options --seed 11814

 FAIL["test_home_should_include_following_and_followers_with_login", HomeTest, 3.5389004000026034]
 test_home_should_include_following_and_followers_with_login#HomeTest (3.54s)
        Expected at least 1 element matching "strong", found 0..
        Expected 0 to be >= 1.
        test/integration/home_test.rb:11:in `block in <class:HomeTest>'

  1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.54956s
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

発展 - ログイン済みユーザーのHomeページに対するテストとして考えられる例

マイクロポスト投稿フォームが表示されていること

test "home should include new micropost form with login" do
  log_in_as @user
  get root_path
  assert_select 'form', id: 'new_micropost'
end

マイクロポスト表示フィードがレンダリングされること

test "home should render micropost feed placeholder with login" do
  log_in_as @user
  get root_path
  assert_select 'div' do
    assert_select 'h3', text: 'Micropost Feed'
  end
end

単純に「テキストが'Micropost Feed'であるh3要素を含むdiv要素が存在すること」についてテストを行っています。

3.2. 同様にして、プロフィールページにもテストを追加してみましょう。

テストコードの内容そのものは、上記演習3.1.のものと同一です。

プロフィールページの表示内容に対するテストの実体はtest/integration/users_profile_test.rbです。前述の内容を踏まえ、test/integration/users_profile_test.rb全体の変更内容は以下のようになります。

test/integration/users_profile_test.rb
  require 'test_helper'

  class UsersProfileTest < ActionDispatch::IntegrationTest
    include ApplicationHelper

    def setup
      @user = users(:rhakurei)
    end

    test "profile display" do
      get user_path(@user)
      assert_template 'users/show'
      assert_select 'title', full_title(@user.name)
      assert_select 'h1', text: @user.name
      assert_select 'h1>img.gravatar'
      assert_match @user.microposts.count.to_s, response.body
+     assert_select 'strong', { id: 'following', text: /#{@user.following.count.to_s}/ }
+     assert_select 'strong', { id: 'followers', text: /#{@user.followers.count.to_s}/ }
      assert_select 'div.pagination', count: 1
      @user.microposts.paginate(page: 1).each do |micropost|
        assert_match micropost.content, response.body
      end
    end
  end

プロフィールページ上の統計情報の描画に対するテストが成功することを確認する

現状の実装では、上記テストは問題なく成功します。

# rails test test/integration/users_profile_test.rb
Running via Spring preloader in process 584
Started with run options --seed 41315

  1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.06740s
1 tests, 71 assertions, 0 failures, 0 errors, 0 skips

プロフィールページ上の統計情報の描画に対するテストが失敗する例

例えば、app/views/users/show.html.erbに以下の欠落がある場合を考えてみます。

app/views/users/show.html.erb(バグあり)
  <% provide(:title, @user.name) %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        ...略
      </section>
      <section>
-       <%= render 'shared/stats' %>
      </section>
    </aside>
    <div class="col-md-8">
      ...略
    </div>
  </div>

この状態でtest/integration/users_profile_test.rbに対するテストを行うと、以下のようにテストが失敗します。

# rails test test/integration/users_profile_test.rb
Running via Spring preloader in process 672
Started with run options --seed 11646

 FAIL["test_profile_display", UsersProfileTest, 4.164021200005664]
 test_profile_display#UsersProfileTest (4.16s)
        Expected at least 1 element matching "strong", found 0..
        Expected 0 to be >= 1.
        test/integration/users_profile_test.rb:17:in `block in <class:UsersProfileTest>'

  1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.17069s
1 tests, 7 assertions, 1 failures, 0 errors, 0 skips

[Following] と [Followers] ページ

ページの基本的な仕様

「フォローしているユーザー一覧」「フォロワー一覧」いずれも、そのレイアウトは類似するものとなります。具体的には、以下の要素が含まれることになります。

  • サイドバー
    • ログインユーザーの基本情報
    • ログインユーザーがフォローしているユーザーの数
    • ログインユーザーのフォロワーの数
    • フォローしているユーザー、またはフォロワーのアイコンを縮小表示して格子状に並べたもの
      • 当該ユーザーのプロフィールページへのリンクが貼られている
  • フォローしているユーザー、もしくはフォロワーのリスト

Railsチュートリアル本文では、フォローしているユーザーの一覧のモックアップを図 14.14で、フォロワーの一覧のモックアップを図 14.15で示しています。

フォロー/フォロワーページの認可のテスト

「フォローしているユーザーの一覧、フォロワーの一覧、いずれのページもログイン済みユーザーでなければアクセスできないこととする」「非ログインユーザーがこれら一覧ページにアクセスしようとした場合、 /login にリダイレクトする」という仕様を採用することをまず前提とします。これはTwitterにおける実装に倣ったものです。

となると、「これらのページへのアクセスにおいて、認可機構が正しく働いているか」のテストが必要となります。情報セキュリティに関する部分の仕様であるだけに、この部分の動作が正しいものであることは重要です。というわけで、実装より先にテストを書いていくこととします。

テストそのものの実体は以下のようになります。

test "should redirect following when not logged in" do
  get following_user_path(@user)
  assert_redirected_to login_url
end

test "should redirect followers when not logged in" do
  get followers_user_path(@user)
  assert_redirected_to login_url
end

追加するテストの実装先は、test/controllers/users_controller_test.rbです。

test/controllers/users_controller_test.rb
  require 'test_helper'

  class UsersControllerTest < ActionDispatch::IntegrationTest

    def setup
      @user       = users(:rhakurei)
      @other_user = users(:mkirisame)
    end

    ...略
+
+   test "should redirect following when not logged in" do
+     get following_user_path(@user)
+     assert_redirected_to login_url
+   end
+
+   test "should redirect followers when not logged in" do
+     get followers_user_path(@user)
+     assert_redirected_to login_url
+   end
  end

現時点でテストが成功しないことの確認

新たに実装したテストは、当然ながら現時点では成功しません。

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 840
Started with run options --seed 47643

ERROR["test_should_redirect_followers_when_not_logged_in", UsersControllerTest, 1.9549646000086796]
 test_should_redirect_followers_when_not_logged_in#UsersControllerTest (1.96s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'followers' could not be found for UsersController
            test/controllers/users_controller_test.rb:80:in `block in <class:UsersControllerTest>'

ERROR["test_should_redirect_following_when_not_logged_in", UsersControllerTest, 2.14522200000647]
 test_should_redirect_following_when_not_logged_in#UsersControllerTest (2.15s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'following' could not be found for UsersController
            test/controllers/users_controller_test.rb:75:in `block in <class:UsersControllerTest>'

  11/11: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.21912s
11 tests, 18 assertions, 0 failures, 2 errors, 0 skips

そもそもfollowingfollowersというアクションはまだ実装していないので、テストが通らないのみ当然といえば当然です。ただ、RoutingErrorではなくActionNotFoundなので、ルーティングの実装は正常に行えているようです。

Usersコントローラーに、followingアクションとfollowersアクションを実装する

先ほどテストで発生したエラーを解決するために、Usersコントローラーにfollowingアクションとfollowersアクションを実装していきます。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    ...略
+
+   def following
+   end
+
+   def followers
+   end

    private
      ...略
  end

Usersコントローラーにfollowingアクションとfollowersアクションがある状態でのテスト結果

この時点でtest/controllers/users_controller_test.rbを対象としてテストを行うと、その結果は以下のようになります。

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 861
Started with run options --seed 8813

ERROR["test_should_redirect_followers_when_not_logged_in", UsersControllerTest, 2.068188299992471]
 test_should_redirect_followers_when_not_logged_in#UsersControllerTest (2.07s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: UsersController#followers is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

        NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
            test/controllers/users_controller_test.rb:80:in `block in <class:UsersControllerTest>'

ERROR["test_should_redirect_following_when_not_logged_in", UsersControllerTest, 4.581023499995354]
 test_should_redirect_following_when_not_logged_in#UsersControllerTest (4.58s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: UsersController#following is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

        NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
            test/controllers/users_controller_test.rb:75:in `block in <class:UsersControllerTest>'

  11/11: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.58386s
11 tests, 18 assertions, 0 failures, 2 errors, 0 skips

上記エラーが発生する状態で、Webブラウザから /users/1/following というリソースにアクセスすると、Railsサーバーには以下のようなログが記録されます。

Started GET "/users/1/following" for 172.17.0.1 at 2020-02-03 22:46:07 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#following as HTML
  Parameters: {"id"=>"1"}
Completed 406 Not Acceptable in 1125ms
...略

HTTPリクエストが406というエラーコードを返して終了している、という状態ですね。「406」というエラーコードの意味はさておき、この処理で返ってくるHTTPのレスポンスコードは「3XX(リダイレクト)」でなければなりません。

Usersコントローラーのfollowingアクションとfollowersアクションに対し、「ログイン済みユーザーでなければログイン画面にリダイレクトする」という動作が行われるようにする

表題記載の動作が行われるようにするためには、Usersコントローラーのbeforeフィルターに以下のコードを追加します。

before_action :logged_in_user, only: [:following, :followers]

実際にapp/controllers/users_controller.rbに適用する変更は以下のようになります。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
-   before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
+   before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]
    before_action :correct_user,   only: [:edit, :update]
    before_action :admin_user,     only: :destroy

    ...略
  end

この時点で、test/controllers/users_controller_test.rbを対象としたテストが成功するようになる

この時点で、test/controllers/users_controller_test.rbを対象としたテストは成功するようになります。

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 958
Started with run options --seed 46648

  11/11: [=================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.49225s
11 tests, 20 assertions, 0 failures, 0 errors, 0 skips

しかしながら、現時点でfollowingアクションおよびfollowersアクションの動作は何も実装していません。これらの動作の実装が必要となります。

followingアクションとfollowersアクションの動作の実装

followingアクションとfollowersアクションの動作に対するテスト

following/followerをテストするためのfixture

test/fixtures/relationships.yml
one:
  follower: rhakurei
  followed: skomeiji

two:
  follower: rhakurei
  followed: rusami

three:
  follower: skomeiji
  followed: rhakurei

four:
  follower: mkirisame
  followed: rhakurei

このfixtureは、以下のようなフォロー関係を定義しています。

  • rhakureiがskomeijiとrusamiをフォローする
  • skomeijiとmkirisameがrhakureiをフォローする

following/followerページに対する統合テストを生成する

「実際にWebブラウザに描画される内容をテストしたい」という場面なので、テストの種類は統合テストとなります。following/followerページのビューの実装が現状存在しないので、対応する統合テストも現状存在しません。まずは必要な統合テストを生成することが始まりですね。テストの名前はfollowingとします。

# rails generate integration_test following
      invoke  test_unit
      create    test/integration/following_test.rb

following/followerページのテストの実体

前項で生成された統合テストのファイル名はtest/integration/following_test.rbとなります。テストそのものの内容は以下のようになります。

test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:rhakurei)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end
end
assert_not @user.following.empty?assert_not @user.followers.empty?というテストの意味合い
assert_not @user.following.empty?

@user.following.empty?の戻り値がtrueである場合、このあとの@user.following.eachブロック内にあるassert_selectというテストが実行されなくなってしまいます。そのような状況で「テストが成功した」と主張するのは不適当です。ゆえに、「@user.following.empty?の戻り値がtrueである場合はテストを失敗させる」という処理を先に実行しています。@user.following.empty?の戻り値がtrueとなる場合には、例えば「fixtureの内容が不適当な場合」があります。

また、@user.followersに対する以下のテストも意味合いは同様です。

assert_not @user.followers.empty?

現状における、test/integration/following_test.rbを対象としたテストの結果

「Usersコントローラーに、following/followers両アクションのみが実装されており、following/followersアクションの処理内容が実装されていない」という状態で、test/integration/following_test.rbを実行してみます。結果は以下のようになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 1077
Started with run options --seed 36731

ERROR["test_followers_page", FollowingTest, 2.2801150000013877]
 test_followers_page#FollowingTest (2.28s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: UsersController#followers is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

        NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
            test/integration/following_test.rb:19:in `block in <class:FollowingTest>'

ERROR["test_following_page", FollowingTest, 3.6601012000028277]
 test_following_page#FollowingTest (3.66s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: UsersController#following is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

        NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
            test/integration/following_test.rb:10:in `block in <class:FollowingTest>'

  2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.66550s
2 tests, 0 assertions, 0 failures, 2 errors, 0 skips

HTTPリクエストが406というエラーコードを返して終了している、という状態ですね。「406」というエラーコードの意味はさておき、この処理で返ってくるHTTPのレスポンスコードは「200」でなければなりません。

Usersコントローラーにおける、followingアクションとfollowersアクションの動作の実装

  • 「誰がフォローしているユーザーか」「誰のフォロワーか」の「誰」の部分については、GETリクエストに渡すパラメータのid属性の値によって与える
  • ユーザー一覧の表示に対し、ページネーション処理を行う

上記箇条書きの内容を前提条件とすると、followingアクションとfollowersアクションの動作の実装内容は以下のようになります。

def following
  @title = "Following"
  @user  = User.find(params[:id])
  @users = @user.following.paginate(page: params[:page])
  render 'show_follow'
end

def followers
  @title = "Followers"
  @user  = User.find(params[:id])
  @users = @user.followers.paginate(page: params[:page])
  render 'show_follow'
end

後述するように、フォローしているユーザーの一覧/フォロワーの一覧とも、一つのERbで両方の場合をカバーできる程度にページ構造は酷似しています。ゆえに、「コントローラーの2つのアクションが同一ビューを描画する」という実装になるわけです。

最終的に、app/controllers/users_controller.rbに対して加える変更の内容は以下のようになります。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]
    before_action :correct_user,   only: [:edit, :update]
    before_action :admin_user,     only: :destroy

    ...略

    def following
+     @title = "Following"
+     @user  = User.find(params[:id])
+     @users = @user.following.paginate(page: params[:page])
+     render 'show_follow'
    end

    def followers
+     @title = "Followers"
+     @user  = User.find(params[:id])
+     @users = @user.following.paginate(page: params[:page])
+     render 'show_follow'
    end

    private

      ...略
  end

followingアクションとfollowersアクションに必要なビューの実装

当然ながら、show_followというビューそのものの実装も必要となります。ファイル名はapp/views/users/show_follow.html.erbとします。

app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

再びtest/integration/following_test.rbを対象としたテストを実行する

Usersコントローラーのfollowingアクションとfollowersアクション、これらのアクションに必要なビュー、以上の実装が完了しました。この時点で、再びtest/integration/following_test.rbを対象としたテストを実行してみましょう。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 1129
Started with run options --seed 47849

  2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.18609s
2 tests, 10 assertions, 0 failures, 0 errors, 0 skips

ここまで実装したコードの内容に間違いがなければ、test/integration/following_test.rbを対象としたテストは成功するはずです。

# rails test
Running via Spring preloader in process 1142
Started with run options --seed 56786

  72/72: [=================================] 100% Time: 00:00:10, Time: 00:00:10

Finished in 10.74545s
72 tests, 357 assertions, 0 failures, 0 errors, 0 skips

テストスイート全体に対するテストも成功しましたね。

following/followerページの表示結果

現在のユーザーにフォローされているユーザーの一覧表示のスクリーンショットを以下に示します。アドレスバー部分を見てのとおり、followingアクションを経由してshow_followビューが呼び出された結果となります。

スクリーンショット 2020-02-05 19.07.38.png

続いて、現在のユーザをフォローしているユーザーの一覧表示のスクリーンショットを以下に示します。アドレスバー部分を見てのとおり、followersアクションを経由してshow_followビューが呼び出された結果となります。

スクリーンショット 2020-02-05 19.07.53.png

ログイン済みであれば、ログインユーザー以外のユーザーに対しても、当該ユーザーをフォローしているユーザーを一覧表示することも可能です。以下のスクリーンショットがその例です。

スクリーンショット 2020-02-05 19.14.19.png

演習 - [Following] と [Followers] ページ

1.1. ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。

以下のスクリーンショットの通りです。

スクリーンショット 2020-02-05 19.07.38.png

スクリーンショット 2020-02-05 19.07.53.png

1.2. /users/1/followers や /users/1/following において、サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?

/users/1/followers における、サイドバーにある単一の画像に対応するHTMLコードは、例えば以下のようになります。

サイドバーにある単一の画像に対応するHTMLコード
<a href="/users/3">
<img alt="Berry Cremin" class="gravatar" src="https://secure.gravatar.com/avatar/2065436fdfe2d27dc7f06b6787a4a1af?s=30">
</a>

リンク先が /users/3 であることを踏まえて、実際に当該画像をクリックしてみます。すると、Railsサーバーは以下のようなログを出力します。

Started GET "/users/3" for 172.17.0.1 at 2020-02-05 22:50:12 +0000
...略
Completed 200 OK in 1276ms (Views: 1154.3ms | ActiveRecord: 69.4ms)

/users/3 へのGETリクエストが発行され、「200 OK」でリクエストが完了しています。「サイドバーにある画像は、リンクとしてうまく機能していることが確認できた」といえそうです。

2. リスト 14.29assert_selectに関連するコードをコメントアウトしてみて、テストが正しくredに変わることを確認してみましょう。

app/views/users/show_follow.html.erbに以下の欠落がある場合、当該テストは、assert_selectのところで失敗するはずです。

app/views/users/show_follow.html.erb
  <% provide(:title, @title) %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= gravatar_for @user %>
        <h1><%= @user.name %></h1>
        <span><%= link_to "view my profile", @user %></span>
        <span><b>Microposts:</b> <%= @user.microposts.count %></span>
      </section>
      <section class="stats">
        <%= render'shared/stats' %>
        <% if @users.any? %>
          <div class="user_avatars">
            <% @users.each do |user|%>
-             <%= link_to gravatar_for(user, size: 30), user %>
            <% end %>
          </div>
        <% end %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3><%= @title %></h3>
      <% if @users.any? %>
        <ul class="users follow">
-         <%= render @users %>
        </ul>
        <%= will_paginate %>
      <% end %>
    </div>
  </div>

実際にテストを実行してみましょう。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 1196
Started with run options --seed 9425

 FAIL["test_followers_page", FollowingTest, 2.336555000001681]
 test_followers_page#FollowingTest (2.34s)
        Expected at least 1 element matching "a[href="/users/919532091"]", found 0..
        Expected 0 to be >= 1.
        test/integration/following_test.rb:23:in `block (2 levels) in <class:FollowingTest>'
        test/integration/following_test.rb:22:in `block in <class:FollowingTest>'

 FAIL["test_following_page", FollowingTest, 2.4256373999960488]
 test_following_page#FollowingTest (2.43s)
        Expected at least 1 element matching "a[href="/users/314048677"]", found 0..
        Expected 0 to be >= 1.
        test/integration/following_test.rb:14:in `block (2 levels) in <class:FollowingTest>'
        test/integration/following_test.rb:13:in `block in <class:FollowingTest>'

  2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.42861s
2 tests, 8 assertions, 2 failures, 0 errors, 0 skips

想定通りの形でテストが失敗しました。

followingアクションおよびfollowersアクションの統合テストにおける、Railsチュートリアル本文記載のテストの不具合

実は、Railsチュートリアル本文のリスト 14.29に記述されているテストには、1つの不具合があります。不具合の内容とその修正については、別記事に記載しています。

[Follow] ボタン (基本編)

Relationshipsコントローラーの作成

「フォロー」「フォロー解除」という動作は、それぞれリレーションシップの作成と削除に対応しています。RESTアーキテクチャを前提とした場合、少なくともcreateアクションとdestroyアクションが確実に必要となる場面ですね。

というわけで、まずはRelationshipsコントローラーの作成から始めます。

# rails generate controller Relationships
Running via Spring preloader in process 1262
      create  app/controllers/relationships_controller.rb
      invoke  erb
      create    app/views/relationships
      invoke  test_unit
      create    test/controllers/relationships_controller_test.rb
      invoke  helper
      create    app/helpers/relationships_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/relationships.coffee
      invoke    scss
      create      app/assets/stylesheets/relationships.scss

Relationshipsコントローラーに対するテストの記述

認可に関係する動作なので、実装に万全を期すために、テストを先に書いてから実装に取り掛かっていくこととしましょう。

test/controllers/relationships_controller_test.rb
require 'test_helper'

class RelationshipsControllerTest < ActionDispatch::IntegrationTest

  test "create should require logged-in user" do
    assert_no_difference 'Relationships.count' do
      post relationships_path
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_differende 'Relationships.count' do
      delete relationship_path(relationships(:one))
    end
    assert_redirected_to login_url
  end
end

テストの記述内容に問題がないのであれば、現時点におけるtest/controllers/relationships_controller_test.rbに対するテストの実行結果は以下のようになります。

# rails test test/controllers/relationships_controller_test.rb
Running via Spring preloader in process 1284
Started with run options --seed 280

ERROR["test_create_should_require_logged-in_user", RelationshipsControllerTest, 1.4866881000052672]
 test_create_should_require_logged-in_user#RelationshipsControllerTest (1.49s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'create' could not be found for RelationshipsController
            test/controllers/relationships_controller_test.rb:7:in `block (2 levels) in <class:RelationshipsControllerTest>'
            test/controllers/relationships_controller_test.rb:6:in `block in <class:RelationshipsControllerTest>'

ERROR["test_destroy_should_require_logged-in_user", RelationshipsControllerTest, 1.673922499991022]
 test_destroy_should_require_logged-in_user#RelationshipsControllerTest (1.67s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'destroy' could not be found for RelationshipsController
            test/controllers/relationships_controller_test.rb:14:in `block (2 levels) in <class:RelationshipsControllerTest>'
            test/controllers/relationships_controller_test.rb:13:in `block in <class:RelationshipsControllerTest>'

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.68059s
2 tests, 0 assertions, 0 failures, 2 errors, 0 skips

AbstractController::ActionNotFoundというエラーが発生しています。Relationshipsコントローラーに、createアクションもdestroyアクションも定義されていないためにエラーが発生しているのですね。

Relationshipsコントローラーに、createアクション・destroyアクション・logged_in_userフィルターを追加する

何はなくとも、まずRelationshipsコントローラーにcreateアクションおよびdestroyアクションの実装が必要となります。logged_in_userフィルターによるアクセス制御も同時に追加します。

app/controllers/relationships_controller.rb
  class RelationshipsController < ApplicationController
+   before_action :logged_in_user
+
+   def create
+   end
+
+   def destroy
+   end
  end

createアクション・destroyアクション・logged_in_userフィルターがあるRelationshipsコントローラーに対するテストの結果

ここまでの実装が完了したところで、現時点のtest/controllers/relationships_controller_test.rbを対象に、改めてテストを実行してみます。

# rails test test/controllers/relationships_controller_test.rb
Running via Spring preloader in process 1323
Started with run options --seed 62701

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.55615s
2 tests, 4 assertions, 0 failures, 0 errors, 0 skips

テストが無事成功しました。

Relationshipsコントローラーの完全な実装

Relationshipsコントローラーの完全な実装、すなわちapp/controllers/relationships_controller.rbの最終的な中身は、以下のようになります。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    user = User.find(params[:id])
    current_user.follow(user)
    redirect_to user
  end

  def destroy
    user = Relationships.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end
end

非ログインユーザーが、Relationshipsリソースに直接POSTDELETEを行った場合の動作

実は、beforeフィルターがない状態でも、「非ログインユーザーがRelationshipsリソースに直接POSTDELETEを実行した場合、RDBの内容に変化は生じない」という動作は実現されています。その流れは以下の通りです。

  1. 非ログインユーザーが(curl)Relationshipsリソースに直接POSTDELETEを実行する
  2. createアクションにせよdestroyアクションにせよ、current_usernilになる
  3. followunfollowが呼び出された時点で例外が発生する

しかしながら、「アプリケーションロジックの正常な動作が、例外の発生に依存したものとなる」というのは避けたいパターンです。しかもそれが、「nilに対する参照」という例外であるならばなおさらです。ゆえに今回は、「beforeフィルターを追加する」という実装を行っています。

演習 - [Follow] ボタン (基本編)

1. ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?

下記が初期状態のスクリーンショットです。「ログインユーザーはid=2のユーザーをフォローしていない」という状態です。

スクリーンショット 2020-02-07 7.52.39.png

[Unfollow]ボタンではなく[Follow]ボタンが表示されていますね。

では[Follow]ボタンを押してみましょう。結果は以下のようになります。

スクリーンショット 2020-02-07 7.52.47.png

[Follow]ボタンではなく[Unfollow]ボタンが表示されていますね。

では[Unfollow]ボタンを押してみましょう。結果は以下のようになります。

スクリーンショット 2020-02-07 7.52.53.png

[Unfollow]ボタンではなく[Follow]ボタンが表示されています。

2. 先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?

フォローが実行されたときの処理として、Railsサーバーには以下のログが出力されています。

Started POST "/relationships" ...略
Redirected to http://localhost:8080/users/2
Completed 302 Found in 65ms (ActiveRecord: 38.6ms)


Started GET "/users/2" for 172.17.0.1 at 2020-02-06 22:52:42 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"2"}
  ...略
  Rendering users/show.html.erb within layouts/application
  ...略
  Rendered shared/_stats.html.erb (16.1ms)
  ...略
  Rendered users/_unfollow.html.erb (9.7ms)
  Rendered users/_follow_form.html.erb (41.2ms)
  ...略
  Rendered users/show.html.erb within layouts/application (168.1ms)
  ...略
Completed 200 OK in 629ms (Views: 576.3ms | ActiveRecord: 28.2ms)

_unfollow.html.erbが描画されている」というのが重要です。「_unfollow.html.erbは、_follow_form.html.erbにおいて、ログインユーザーが対象のユーザーをフォローしている場合に描画される」ように実装したのでしたよね。

一方、フォロー解除が実行されたときの処理としては、Railsサーバーには以下のログが出力されています。

Started DELETE "/relationships/88" ...略
Redirected to http://localhost:8080/users/2
Completed 302 Found in 55ms (ActiveRecord: 36.7ms)


Started GET "/users/2" for 172.17.0.1 at 2020-02-06 22:52:49 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"2"}
  ...略
  Rendering users/show.html.erb within layouts/application
  ...略
  Rendered shared/_stats.html.erb (12.9ms)
  ...略
  Rendered users/_follow.html.erb (2.0ms)
  Rendered users/_follow_form.html.erb (30.1ms)
  ...略
  Rendered collection of microposts/_micropost.html.erb [30 times] (14.1ms)
  ...略
  Rendered users/show.html.erb within layouts/application (160.8ms)
  ...略
Completed 200 OK in 881ms (Views: 828.7ms | ActiveRecord: 24.9ms)

_follow.html.erbが描画されている」というのが重要です。「_follow.html.erbは、_follow_form.html.erbにおいて、ログインユーザーが対象のユーザーをフォローしていない場合に描画される」ように実装したのでしたよね。

なお、参考として、app/views/users/_follow_form.html.erbそのものの実装内容は以下のようになっていることを明記しておきます。

app/views/users/_follow_form.html.erb
<% unless current_user?(@user) %>
  <div id="follow_form">
    <% if current_user.following?(@user) %>
      <%= render 'unfollow' %>
    <% else %>
      <%= render 'follow' %>
    <% end %>
  </div>
<% end %>

[Follow] ボタン (Ajax編)

現状の[Follow]/[Unfollow]ボタンの実装の問題点

現状の[Follow]/[Unfollow]ボタンの実装では、「ボタンをクリックした後、ログインユーザー自身のプロフィールページにリダイレクトされる」という動作になっています。

しかしながら、[Follow]/[Unfollow]ボタンが表示されているのは、専用の投稿フォームではなく、任意のユーザーのプロフィールページです。「何らかのアクションをとると、勝手にページの移動が発生する」という挙動は、フォーム以外に内容のないページならともかく、そうでないページの場合はユーザーの期待に反する動作である可能性が高いです。

このような場合は、「ページ移動が発生せず、[Follow]/[Unfollow]ボタンのあるページに留まる」という実装のほうが望ましいのではないでしょうか。

Ajaxを使えば、上記の問題点に対し、より望ましい形の実装に持っていける

Ajaxを使えば、WebブラウザとWebサーバーの間での「非同期」処理が可能になります。「ページを移動することなくリクエストを送信する」という処理です。「ページ移動が発生せず、[Follow]/[Unfollow]ボタンのあるページに留まる」という処理も、Ajaxによって実現が可能です。

RailsにおけるAjaxの利用

Railsにおいても、Ajaxの利用は容易に可能です。ビューに記述されているform_forメソッドにremote: trueというオプションを追加すれば、それだけでRailsアプリケーションは自動的にAjaxを使うようになります。

form_for ..., remote: true

Ajaxを使ったフォローフォーム・フォロー解除フォーム

Ajaxを使ったフォローフォームのコードは以下のようになります。

app/views/users/_follow.html.erb
- <%= form_for(current_user.active_relationships.build) do |f| %>
+ <%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
    <div><%= hidden_field_tag :followed_id, @user.id %></div>
    <%= f.submit "Follow", class: "btn btn-primary" %>
  <% end %>

一方、Ajaxを使ったフォロー解除フォームのコードは以下のようになります。

app/views/users/_unfollow.html.erb
  <%= form_for(current_user.active_relationships.find_by( followed_id: @user.id),
-                                                         html:{ method: :delete })
+                                                         html:{ method: :delete },
+                                                         remote:true)
  do |f| %>
    <%= f.submit "Unfollow", class: "btn" %>
  <% end %>

上記埋め込みRubyで生成されるHTMLの内容

上記の埋め込みRubyでは、例えば以下のようなHTMLが生成されます。

<form class="new_relationship" id="new_relationship" action="/relationships" accept-charset="UTF-8" data-remote="true" method="post">
...略
</form>

formタグの内部でdata-remote="true"が設定されている」というのがポイントです。この属性設定は、「JavaScriptによるフォーム操作を許可することをRailsに知らせる」という意味があります。

コントローラー側のAjax対応

respond_toメソッド

Ajaxに対応するためには、コントローラー側の実装も一部変更する必要があります。具体的には、「respond_toメソッドを使い、リクエストの種類によって応答を場合分けする」という実装が必要になります。respond_toメソッドの基本的な用法は以下のようになります。

respond_to do |format|
  format.html { redirect_to user }
  format.js
end

respond_toは引数としてブロックを取りますが、その動作は「ブロック内のコードのうち、いずれかの1行が処理される」というものになります。

Relationshipsコントローラーの実装を変更する

Relationshipsコントローラーの実体であるapp/controllers/relationships_controller.rbの内容は、以下のように変更します。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

「Relationshipsコントローラーのアクションで使われる変数を、ローカル変数のuserではなくインスタンス変数の@userに変更した」という点には注意が必要です。

user@userに変更した理由を説明するにあたっては、「app/views/users/_follow_form.html.erbというビューは、@userの内容に応じて動作を分岐させるという実装である」というのが重要なポイントです。この実装を踏まえると、「ページ遷移が発生することなしに、[follow]/[unfollow]ボタンの描画状態に変化が発生する」というユースケースを実現するためには、Relationshipsコントローラーのアクションで直接@userを書き換える必要が出てきます。そのためuser@userに変更する必要が発生した、という次第です。

WebブラウザでJavaScriptが無効に設定されていた場合のための、Railsの設定の変更

Webブラウザ側でJavaScriptが無効にされていると、当然ながらWebブラウザでAjaxリクエストを発行することはできません。RailsアプリケーションでAjax対応を前提とした実装を行った場合、JavaScript無効のWebブラウザでアプリケーションを動かすためには、Rails側の設定を変更する必要があります。具体的には、「認証トークンがremoteフォームに埋め込まれるようにする」必要があります。

# 認証トークンをremoteフォームに埋め込む
config.action_view.embed_authenticity_token_in_remote_forms = true

変更対象となるファイルはconfig/application.rbです。

config/application.rb
  require_relative 'boot'

  require 'rails/all'

  ...略

  module SampleApp
    class Application < Rails::Application
      ...略
+
+     # 認証トークンをremoteフォームに埋め込む
+     config.action_view.embed_authenticity_token_in_remote_forms = true
    end
  end

Ajaxリクエストを受信したときに呼び出される埋め込みRubyファイルの実装

生成すべきファイルの名前

まずは前提知識から。RailsアプリケーションがHTTPのGETリクエストを受信すると、対応するアクション(indexshownewedit)と同じ名前を持つHTML用の埋め込みRuby(例えばshowアクションに対するshow.html.erb)が自動で呼び出されます。Railsチュートリアルを第14章まで進めてきた人であれば、ここまでは既知かと思います。

RailsアプリケーションがAjaxリクエストを受信した場合も、その動作はHTTPのGETリクエストに対する動作と酷似したものになります。すなわち、「対応するアクションと同じ名前を持つ埋め込みRubyが自動で呼び出される」という動作をするのです。但し、Ajaxリクエストに対する動作の場合は、「呼び出される埋め込みRubyは、HTML用ではなくてJavascript用のものとなる」という違いがあります。

「Ajaxリクエストに対して実行される動作の内容の定義と、各動作に対応するerbファイルの名前」は、今回の場合、以下のような関係になります。

動作 対応するアクションの内容 対応するファイル名
フォロー Relationshipオブジェクトのcreate app/views/relationships/create.js.erb
フォロー解除 Relationshipオブジェクトのdestroy app/views/relationships/destroy.js.erb

jQueryによるDOM操作

前提となる記法

$("#follow_form")

上記の$(#follow_form)というオブジェクトは、「follow_formというCSS idを持つ要素」を指します。フォームそのものを指すものではありません。なお、クラスを指す場合は、#の代わりに.を用います。こちらもCSSと同様ですね。

$("#follow_form").html("foobar")

例えば、follow_formというCSS idを持つフォロー用フォーム全体を"foobar"という文字列で置き換えたい場合、以上のようなコードを使います。

 create.js.erbdestroy.js.erbの実際の中身

app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('user/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html("<%= @user.followers.count %>");

上記コードのポイントは以下です。

  • JS-ERbでは、素のJavaScriptとは異なり、組み込みRubyを使うことができる
  • JS-ERbでRailsのrenderメソッドを使ってJavaScriptファイル内にHTMLを挿入する際には、escape_javascriptメソッドで「JavaScriptのダメ文字」をエスケープする必要がある

Ajaxによる[Follow]/[Unfollow]ボタンの実装における注意事項

Ajaxによる[Follow]/[Unfollow]ボタンの実装が完了したら、一旦開発環境のサンプルアプリケーションからログアウトした上で、Railsサーバーを再起動し、再度ログインしましょう。

Ajaxによる[Follow]/[Unfollow]ボタンの実装後、[Unfollow]ボタンからDELETEリクエストを発行する動作が正常に行われるようにするためには、おそらく「ログアウト→再ログイン」という操作が必要となります。そうでないと、「/relationships/:id に対し、DELETEではなくPOSTを発行してしまい、ActionController::RoutingErrorが発生する」という事態になります。

演習 - [Follow] ボタン (Ajax編)

1. ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。

スクリーンショット 2020-02-14 8.25.10.png

followersの数は0で、[Follow]ボタンが表示されています。

ここで[Follow]ボタンをクリックしてみます。次に出た画面のスクリーンショットは以下です。

スクリーンショット 2020-02-14 8.25.21.png

followersの数が1増え、[Follow]ボタンが[Unfollow]ボタンに変わりました。

ここで[Unfollow]ボタンをクリックしてみます。次に出た画面のスクリーンショットは以下です。

スクリーンショット 2020-02-14 8.25.16.png

followersの数が1減り、[Unfollow]ボタンが[Follow]ボタンに変わりました。

2. 先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。

フォローを実行した直後のログ

[Follow]ボタンをクリックし、POSTリクエストが発行されてから、リクエストが完了するまでのRailsサーバーのログを以下に示します。

Started POST "/relationships" ...略
Processing by RelationshipsController#create as JS
  ...略
  Rendering relationships/create.js.erb
  ...略
  Rendered users/_unfollow.html.erb (4.6ms)
  ...略
  Rendered relationships/create.js.erb (30.4ms)
Completed 200 OK in 137ms (Views: 66.4ms | ActiveRecord: 35.8ms)

relationships/create.js.erbの描画が行われ、その中でusers/_unfollow.html.erbの描画が行われる」という順序でテンプレートの描画が行われたことがわかります。

フォロー解除を実行した直後のログ

[Follow]ボタンをクリックし、POSTリクエストが発行されてから、リクエストが完了するまでのRailsサーバーのログを以下に示します。

Started DELETE "/relationships/93" ...略
Processing by RelationshipsController#destroy as JS
  ...略
  Rendering relationships/destroy.js.erb
  Rendered users/_follow.html.erb (1.5ms)
  ...略
  Rendered relationships/destroy.js.erb (30.9ms)
Completed 200 OK in 178ms (Views: 78.8ms | ActiveRecord: 50.1ms)

relationships/destroy.js.erbの描画が行われ、その中でusers/_follow.html.erbの描画が行われる」という順序でテンプレートの描画が行われたことがわかります。

フォローをテストする

ユーザーのフォローに対するテスト

ユーザーのフォローに対するテストの核心は以下のコードです。

assert_difference '@user.following.count', 1 do
  post relationships_path, params: { followed_id: @other.id }
end

テストの構造は、「/relationships に対してPOSTリクエストを発行し、それに対してRDBのレコード数が増えていることをテストする」というものになります。

Ajax版のテストは、以下の内容になります。通常版のテストとの違いは、postメソッドにおけるxhr: trueというオプションの有無だけです。

assert_difference '@user.following.count', 1 do
  post relationships_path, params: { followed_id: @other.id }, xhr: true
end

ユーザーのフォロー解除に対するテスト

relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship)
end

「HTTPリクエストを発行し、それに対するRDBのレコード数の増減をテストする」というテストの構造は、前述「ユーザーのフォローに対するテスト」と類似したものとなります。より具体的な手順は以下の通りになります。

  1. 1人のユーザーをフォローする
  2. 1.で生成されたRelationshipモデルのオブジェクトを、relationship変数に代入する
  3. relationship変数を引数としてDELETEリクエストを発行し、フォロー数が1減ったことをテストする

2.の操作は、「ログインユーザーの能動的リレーションシップから、1.でフォローしたユーザーのidを検索する」という操作により行われます。

relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship), xhr: true
end

Ajax版のテストは、以下の内容になります。通常版のテストとの違いは、postメソッドにおけるxhr: trueというオプションの有無だけです。

test/controllers/relationships_controller_test.rbに対する変更の内容

上記を踏まえると、test/controllers/relationships_controller_test.rb全体に対する変更の内容は、以下のようになります。

test/controllers/relationships_controller_test.rb
  require 'test_helper'

  class FollowingTest < ActionDispatch::IntegrationTest
    def setup
      @user  = users(:rhakurei)
      @other = users(:mkirisame)
      log_in_as(@user)
    end

    test "following page" do
      get following_user_path(@user)
      assert_not @user.following.empty?
      assert_match @user.following.count.to_s, response.body
      @user.following.each do |user|
        assert_select "a[href=?]", user_path(user), minimum: 2
      end
    end

    test "followers page" do
      get followers_user_path(@user)
      assert_not @user.followers.empty?
      assert_match @user.followers.count.to_s, response.body
      @user.followers.each do |user|
        assert_select "a[href=?]", user_path(user), minimum: 2
      end
    end
+
+   test "should follow a user the standard way" do
+     assert_difference '@user.following.count', 1 do
+       post relationships_path, params: { followed_id: @other.id }
+     end
+   end
+   test "should follow a user with Ajax" do
+     assert_difference '@user.following.count', 1 do
+       post relationships_path, xhr: true, params: { followed_id: +other_id }
+     end
+   end
+   test "should unfollow a user the standard way" do
+     @user.follow(@other)
+     relationship = @user.active_relationships.find_by(followed_id: +other.id)
+     assert_difference '@user.following.count', -1 do
+       delete relationship_path(relationship)
+     end
+   end
+   test "should unfollow a user with Ajax" do
+     @user.follo(@other)
+     relationship = @user.active_relationships.find_by(followed_id: other.id)
+     assert_difference '@user.following count', -1 do
+       delete relationship_path(relationship), xhr: true
+     end
+   end
  end

演習 - フォローをテストする

別記事で解説します。

rapidliner00
* 現在のところは、エンジニアに憧れる非エンジニア * エンジニア的な業務効率化・改善に興味あり
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away