フォローのサンプルデータ
別記事で解説します。
演習 - フォローのサンプルデータ
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 へのアクセス結果は以下の通りです。
対応する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 へのアクセス結果は以下の通りです。
対応する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]ボタンをレンダリングする場所そのものが確保されず、これらのボタンも表示されないはずです。どうなっているでしょうか。
確かに想定通りの動作になっています。
対応する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」という表示内容に問題はありません。
このとき、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」という表示内容に問題はありません。
このとき、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
です。
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
に以下の欠落がある場合を考えてみます。
<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
全体の変更内容は以下のようになります。
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
に以下の欠落がある場合を考えてみます。
<% 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
です。
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
そもそもfollowing
やfollowers
というアクションはまだ実装していないので、テストが通らないのみ当然といえば当然です。ただ、RoutingError
ではなくActionNotFound
なので、ルーティングの実装は正常に行えているようです。
Usersコントローラーに、following
アクションとfollowers
アクションを実装する
先ほどテストで発生したエラーを解決するために、Usersコントローラーにfollowing
アクションとfollowers
アクションを実装していきます。
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
に適用する変更は以下のようになります。
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
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
となります。テストそのものの内容は以下のようになります。
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
に対して加える変更の内容は以下のようになります。
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
とします。
<% 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
ビューが呼び出された結果となります。
続いて、現在のユーザをフォローしているユーザーの一覧表示のスクリーンショットを以下に示します。アドレスバー部分を見てのとおり、followers
アクションを経由してshow_follow
ビューが呼び出された結果となります。
ログイン済みであれば、ログインユーザー以外のユーザーに対しても、当該ユーザーをフォローしているユーザーを一覧表示することも可能です。以下のスクリーンショットがその例です。
演習 - [Following] と [Followers] ページ
1.1. ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。
以下のスクリーンショットの通りです。
1.2. /users/1/followers や /users/1/following において、サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?
/users/1/followers における、サイドバーにある単一の画像に対応する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.29のassert_select
に関連するコードをコメントアウトしてみて、テストが正しくred
に変わることを確認してみましょう。
app/views/users/show_follow.html.erb
に以下の欠落がある場合、当該テストは、assert_select
のところで失敗するはずです。
<% 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コントローラーに対するテストの記述
認可に関係する動作なので、実装に万全を期すために、テストを先に書いてから実装に取り掛かっていくこととしましょう。
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
フィルターによるアクセス制御も同時に追加します。
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
の最終的な中身は、以下のようになります。
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リソースに直接POST
やDELETE
を行った場合の動作
実は、beforeフィルターがない状態でも、「非ログインユーザーがRelationshipsリソースに直接POST
やDELETE
を実行した場合、RDBの内容に変化は生じない」という動作は実現されています。その流れは以下の通りです。
- 非ログインユーザーが(
curl
)Relationshipsリソースに直接POST
やDELETE
を実行する -
create
アクションにせよdestroy
アクションにせよ、current_user
はnil
になる -
follow
やunfollow
が呼び出された時点で例外が発生する
しかしながら、「アプリケーションロジックの正常な動作が、例外の発生に依存したものとなる」というのは避けたいパターンです。しかもそれが、「nil
に対する参照」という例外であるならばなおさらです。ゆえに今回は、「beforeフィルターを追加する」という実装を行っています。
演習 - [Follow] ボタン (基本編)
1. ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?
下記が初期状態のスクリーンショットです。「ログインユーザーはid=2のユーザーをフォローしていない」という状態です。
[Unfollow]ボタンではなく[Follow]ボタンが表示されていますね。
では[Follow]ボタンを押してみましょう。結果は以下のようになります。
[Follow]ボタンではなく[Unfollow]ボタンが表示されていますね。
では[Unfollow]ボタンを押してみましょう。結果は以下のようになります。
[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
そのものの実装内容は以下のようになっていることを明記しておきます。
<% 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を使ったフォローフォームのコードは以下のようになります。
- <%= 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を使ったフォロー解除フォームのコードは以下のようになります。
<%= 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
の内容は、以下のように変更します。
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
です。
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
リクエストを受信すると、対応するアクション(index
、show
、new
、edit
)と同じ名前を持つ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.erb
とdestroy.js.erb
の実際の中身
$("#follow_form").html("<%= escape_javascript(render('user/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
$("#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 にアクセスし、うまく動いているかどうか確認してみましょう。
followersの数は0で、[Follow]ボタンが表示されています。
ここで[Follow]ボタンをクリックしてみます。次に出た画面のスクリーンショットは以下です。
followersの数が1増え、[Follow]ボタンが[Unfollow]ボタンに変わりました。
ここで[Unfollow]ボタンをクリックしてみます。次に出た画面のスクリーンショットは以下です。
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.で生成されたRelationshipモデルのオブジェクトを、
relationship
変数に代入する -
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
全体に対する変更の内容は、以下のようになります。
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
演習 - フォローをテストする
別記事で解説します。