何をするか
Usersコントローラーにindex
アクションを実装していきます。これにより、「すべてのユーザーの一覧表示」という操作が可能になります。
追加機能として、以下の機能も実装していきます。
- RDBへのサンプルユーザーの追加
- ページネーション機能
- 英語ではpagination=ページ分割
- ユーザー一覧の出力を複数ページに分割する機能のこと
Railsチュートリアル本文においては、ユーザー一覧ページのモックアップとして、図 10.8が示されています。
ユーザー情報のセキュリティモデル
Railsチュートリアル本文においては、現在開発中のサンプルアプリケーションにおいて、ユーザー情報のセキュリティモデルは以下のように設計することとしています。
- ユーザーの
show
ページは、非ログインユーザーも含め、すべての利用者に表示を許可する - ユーザーの
index
ページは、ログイン済のユーザーのみに表示を許可する
ユーザーの一覧ページ
「ユーザーのindex
ページは、ログイン済のユーザーのみに表示を許可する」という動作に対するテスト
表題内容の対偶をとると、「非ログインユーザーがユーザーのindex
ページを表示しようとした場合、表示を許可しない」という内容になります。「表示を許可しない」という場合の動作は、「ログインページにリダイレクトする」というのが最も自然ですね。
ということで、「非ログインユーザーがユーザーのindex
ページを表示しようとした場合」に対応するテストの実装は、以下のようになります。名前は「should redirect index when not logged in」とします。
test "should redirect index when not logged in" do
get users_path
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 index when not logged in" do
+ get users_path
+ assert_redirected_to login_url
+ end
...略
end
現時点で、このテストは失敗します。
test "should redirect index when not logged in" do
# rails test test/controllers/users_controller_test.rb:10
Running via Spring preloader in process 1376
Started with run options --seed 60339
ERROR["test_should_redirect_index_when_not_logged_in", UsersControllerTest, 0.728221700002905]
test_should_redirect_index_when_not_logged_in#UsersControllerTest (0.73s)
AbstractController::ActionNotFound: AbstractController::ActionNotFound: The action 'index' could not be found for UsersController
test/controllers/users_controller_test.rb:11:in `block in <class:UsersControllerTest>'
6/6: [===================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.73096s
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
現時点でUsersコントローラーにindex
アクションは実装されていないため、「Usersコントローラーにindex
アクションが実装されていない」旨のエラーメッセージが出力されます。
Usersコントローラーにindex
アクションを実装し、beforeフィルターの対象とする
続いてindex
アクションを実装していきます。index
アクションの動作は、現状ではRailsデフォルトの動作とするため、実装は何も記述せずにメソッド定義だけ記述することとします。
「ユーザーのindex
ページは、ログイン済のユーザーのみに表示を許可する」という動作は、「index
アクションを、beforeフィルターのlogged_in_user
に追加し、同メソッドによる保護対象とする」ことにより実現できます。早速app/controllers/users_controller.rb
の内容を修正しましょう。
class UsersController < ApplicationController
- before_action :logged_in_user, only: [:edit, :update]
+ before_action :logged_in_user, only: [:index, :edit, :update]
before_action :correct_user, only: [:edit, :update]
+
+ def index
+ end
...略
end
ここまでの実装が完了すると、テスト「should redirect index when not logged in」が成功するようになります。
# rails test test/controllers/users_controller_test.rb:10
Running via Spring preloader in process 1402
Started with run options --seed 22324
6/6: [===================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.54567s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
indexビューを実装する
Usersコントローラーでindex
アクションの実装が済んだら、今度はindexビューを実装します。中身は「全ユーザーが格納された変数を作成し、順々に表示する」というものになります。
indexビューを実装するために、Usersコントローラーのindex
アクションに必要となる新たな実装
以下の動作をUsersコントローラーのindex
アクションにに実装する必要があります。
-
User.all
を用い、RDBに保存された全ユーザーの情報を取得する - 取得したユーザー情報を、ビューで使えるユーザー変数
@users
に代入する
コードとしては以下のようになります。
@users = User.all
早速Usersコントローラーのindex
アクションに反映しましょう。
class UsersController < ApplicationController
...略
def index
+ @users = User.all
end
...略
end
indexビューを実際に実装する
indexビューのファイル名はapp/views/users/index.html.erb
となります。現時点で当該ファイルは存在しないため、まず当該ファイルを新規に作成するところから始まります。
>>> touch app/views/users/index.html.erb
空のapp/views/users/index.html.erb
が作成できたら、埋め込みRubyを記述していきます。
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
上述埋め込みRubyコードで、特筆すべき点は以下です。
- ユーザーを列挙する領域全体は
ul
要素としている -
each
メソッドでユーザーを列挙している - 各ユーザーのプロフィール画像と名前を
li
タグで囲っている -
UsersHelper#gravatar_for
の:size
オプションに、デフォルト以外の値を与えている- デフォルトは80、今回与えた値は50
(参考)UsersHelper#gravatar_for
の現状の実装
なお、私の環境では、現状のUsersHelper#gravatar_for
の実装は以下のようになっています。
def gravatar_for(user, options = { size:80 })
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
size = options[:size]
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
ユーザーのindexページの表示を整えるために、CSSに手を加える
...略
+
+ .users {
+ list-style: none;
+ margin: 0;
+ li {
+ overflow: auto;
+ padding: 10px 0;
+ border-bottom: 1px solid $gray-lighter;
+ }
+ }
...略
ビューのヘッダー部に、ユーザー一覧ページへのリンクを追加する
ビューのヘッダー部には、すでにユーザー一覧ページへのリンクが存在します。リンク先は、現時点まで仮に#
としてきましたが、ここまでの実装でユーザー一覧ページが表示できるようになったので、実際のユーザー一覧ページにリンクするようにします。
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
...略
<nav>
<ul class="nav navbar-nav navbar-right">
...略
<% if logged_in? %>
- <li><%= link_to "Users", '#' %></li>
+ <li><%= link_to "Users", users_path %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: :delete %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
実際に動くユーザーのindex
ここまでの実装が完了すれば、ユーザーのindexは実際に動くようになります。
# rails test
Running via Spring preloader in process 1441
Started with run options --seed 33187
36/36: [=================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.18539s
36 tests, 96 assertions, 0 failures, 0 errors, 0 skips
新たに実装したテストも含め、すべてのテストが無事通ることが確認できました。

上記はログインユーザーでusersページを表示したスクリーンショットです。
演習 - ユーザーの一覧ページ
1. レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。
ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。
テストを実装するソースファイルはtest/integration/site_layout_test.rb
となります。
ログイン状態の再現に必要な処理
ログイン状態を再現するために、まずはsetup
メソッドにより、fixtureから@user
に有効なユーザー情報を与える必要があります。対応するコードは以下になります。
def setup
@user = users(:rhakurei)
end
ログイン済みユーザー・非ログインユーザー共通に必要となるテスト
リクエストの内容は、「当該レイアウトを使うリソースへのGET
リクエスト」であれば何でも構いません。今回は「/ へのGET
リクエスト」とします。
get root_path
Home・Help・About・Contactへのリンクは、ログイン済みユーザー・非ログインユーザー関係なく共通です。対応するコードは以下になります。
assert_select "a[href=?]", root_path
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
非ログインユーザーに必要となるテスト
ログイン済みユーザーでない場合、レイアウトに存在するリンクは以下の要件を満たす必要があります。
- ログインページへのリンクが存在すること
- ユーザー一覧ページへのリンクが存在しないこと
- ユーザー情報ページへのリンクが存在しないこと
- ユーザー情報編集ページへのリンクが存在しないこと
- ログアウトページへのリンクが存在しないこと
対応するコードは以下になります。
assert_select "a[href=?]", login_path
assert_select "a[href=?]", users_path, count: 0
assert_select "a[href=?]", user_path(@user), count: 0
assert_select "a[href=?]", edit_user_path(@user), count: 0
assert_select "a[href=?]", logout_path, count: 0
ログイン済みユーザーに必要となるテスト
ログイン済みユーザーの場合、レイアウトに存在するリンクは以下の要件を満たす必要があります。
- ログインページへのリンクが存在しないこと
- ユーザー一覧ページへのリンクが存在すること
- 自身のユーザー情報ページへのリンクが存在すること
- 自身のユーザー情報編集ページへのリンクが存在しないこと
- ログアウトページへのリンクが存在すること
対応するコードは以下になります。
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", users_path
assert_select "a[href=?]", user_path(@user)
assert_select "a[href=?]", edit_user_path(@user)
assert_select "a[href=?]", logout_path
test/integration/site_layout_test.rb
の変更内容
test/integration/site_layout_test.rb
の変更内容の全体像は以下のようになります。
require 'test_helper'
class SiteLayoutTest < ActionDispatch::IntegrationTest
+
+ def setup
+ @user = users(:rhakurei)
+ end
...略
+
+ test "layout links without login" do
+ get root_path
+ assert_select "a[href=?]", root_path
+ assert_select "a[href=?]", help_path
+ assert_select "a[href=?]", about_path
+ assert_select "a[href=?]", contact_path
+ assert_select "a[href=?]", login_path
+ assert_select "a[href=?]", users_path, count: 0
+ assert_select "a[href=?]", user_path(@user), count: 0
+ assert_select "a[href=?]", edit_user_path(@user), count: 0
+ assert_select "a[href=?]", logout_path, count: 0
+ end
+
+ test "layout links with login" do
+ log_in_as @user
+ get root_path
+ assert_select "a[href=?]", root_path
+ assert_select "a[href=?]", help_path
+ assert_select "a[href=?]", about_path
+ assert_select "a[href=?]", contact_path
+ assert_select "a[href=?]", login_path, count: 0
+ assert_select "a[href=?]", users_path
+ assert_select "a[href=?]", user_path(@user)
+ assert_select "a[href=?]", edit_user_path(@user)
+ assert_select "a[href=?]", logout_path
+ end
end
今実装したテストが成功することを確認する
ここまで実装が完了すれば、test/integration/site_layout_test.rb
全体を使ったテストを実行できるようになります。ここまでに記述してきた振る舞いは、本当に正しいのでしょうか。実際にやってみましょう。
# rails test test/integration/site_layout_test.rb
Running via Spring preloader in process 1584
Started with run options --seed 33479
3/3: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.33232s
3 tests, 25 assertions, 0 failures, 0 errors, 0 skips
テストは無事成功しました。ここまでに記述してきた振る舞いは確かに正しいようです。
サンプルのユーザー
Fakerというgem
Fakerはダミーデータを生成するためのgemです。「開発環境などで、開発中のアプリケーションの振る舞いを検証するため、ある程度まとまった数のデータが欲しい」という場合に便利です。
Gemfile
にFakerを追加する
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
+ gem 'faker', '1.7.3'
...略
group :development do
...略
end
...略
Faker gemは、通常開発環境でしか使わないため、通常はGemfileの:development
グループ以下に追加します。しかしながら、Railsチュートリアルのサンプルプログラムにおいては、「本番環境でFakerを使う」という例外的な運用が発生するため、Faker gemをGemfileのグループ外に追加しています。
bundle install
でFakerをインストールする
FakerをGemfileに追加したら、例によってbundle install
を実行します。
# bundle install
...略
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Bundler could not find compatible versions for gem "i18n":
In snapshot (Gemfile.lock):
i18n (= 1.6.0)
In Gemfile:
rails (= 5.1.6) was resolved to 5.1.6, which depends on
activesupport (= 5.1.6) was resolved to 5.1.6, which depends on
i18n (< 2, >= 0.7)
faker (= 1.7.3) was resolved to 1.7.3, which depends on
i18n (~> 0.5)
Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
うまくいきませんでした。RailsとFakerで、それぞれが別に依存するi18n
というgemの要求バージョンが合わないことが原因のようです。
指示通りにbundle update
を実行してみます。
# bundle update
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies.....
...略
Fetching faker 1.7.3
Installing faker 1.7.3
...略
Bundle updated!
Gems in the group production were not installed.
今度こそ無事にFaker gemがインストールできたようです。
100人のサンプルユーザーをRDB上に生成する
db/seeds.rb
というファイルに、100人のサンプルユーザーを生成するためのコードを追加します。サンプルユーザーの内訳は以下のとおりです。
- 「Example User」という名前とメールアドレスを持つ1人のユーザー
- それらしい名前とメールアドレスを持つ99人のユーザー
User.create(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!( name: name,
email: email,
password: password,
password_confirmation: password)
end
このコードのポイントは以下です。
-
create
ではなくcreate!
メソッドを用いている- ユーザーが無効な場合に
nil
を返すのではなく、例外を投げる - エラーを検知できる可能性を高めることを狙っている
- ユーザーが無効な場合に
RDBのリセットと、100人のユーザーの生成
RDBをリセットした上で、上記db/seeds.rb
の内容に基づき、実際に100人のユーザーをサンプルアプリケーション上に生成してみます。
まずはRDBをリセットします。
# rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
...略
== [timestamp] AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
-> 0.0116s
== [timestamp] AddRememberDigestToUsers: migrated (0.0119s) ================
続いて、実際にdb/seeds.rb
の内容をRDBに反映します。コマンドはrails db:seed
です。
# rails db:seed
エラーがなければ、シェルには何も表示されずに実行が終わります。
100人のユーザーが生成された!
ログインしてindexページを表示してみましょう。

Michael Hartl氏の手により、最初のいくつかのメールアドレスには、デフォルトのGravatar画像以外の写真が関連付けられています。その写真もきちんと表示されていますね。
# rails console --sandbox
>> User.count
(1.1ms) SELECT COUNT(*) FROM "users"
=> 100
rails console
からUser.count
を実行した結果も、きちんと100
が返ってきました。
演習 - サンプルのユーザー
1. 試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。
現在、id=1のユーザーでログインしていることを前提とします。
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
controller: users
action: show
id: '1'
permitted: false
/users/1/edit へのGET
リクエストに対しては、最終的に「200 OK」のステータスコードが返ってきます。
Started GET "/users/1/edit" ...略
Completed 200 OK in 648ms (Views: 604.0ms | ActiveRecord: 9.6ms)
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
controller: users
action: edit
id: '1'
permitted: false
続いて、/users/2/edit にGET
リクエストを送出してみます。
Started GET "/users/2/edit" ...略
Redirected to http://localhost:8080/
Filter chain halted as :correct_user rendered or redirected
Completed 302 Found in 14ms (ActiveRecord: 8.1ms)
Started GET "/" for ...略
Completed 200 OK in 378ms (Views: 351.2ms | ActiveRecord: 2.8ms)
「ルートURLにリダイレクトされる」という動作は、確かに実装した通りですね。
ページネーション
ページネーションの必要性
「1つのページに大量のユーザーが表示される」というのは、検索ロボットや自動処理用のスクリプトによるアクセスであればともかく、人間が使う上では非常に使いづらいです。100人でも人間が扱うには多いですし、今後ユーザー数が数千人に増える可能性もあります。1つのページに表示されるユーザーの数は、人間が扱いやすい人数、例えば30人にしたいところです。
そうした要求を実現する機能がページネーション(pagination)です。Railsチュートリアル本文では、will_paginateメソッドによるページネーションを実装する、としています。
ページネーションの実装に必要なgemの追加
前提として、ページネーションの実装は以下の条件で行うものとします。
- ページネーションには
will_paginate
メソッドを用いる - Bootstrapのページネーションスタイルを適用する
上記前提条件の元で新たに必要となるgemは、will_paginate
とbootstrap-will_paginate
の2つになります。早速Gemfileに追加しましょう。
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
gem 'faker', '1.7.3'
+ gem 'will_paginate', '3.1.6'
+ gem 'bootstrap-will_paginate', '1.0.0'
...略
続いてbundle install
を実行します。
# bundle install
...略
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies....
...略
Fetching will_paginate 3.1.6
Installing will_paginate 3.1.6
Fetching bootstrap-will_paginate 1.0.0
Installing bootstrap-will_paginate 1.0.0
...略
Bundle complete! 26 Gemfile dependencies, 84 gems now installed.
Gems in the group production were not installed.
Bundled gems are installed into `/usr/local/bundle`
問題なくgemの追加が完了したようですね。
ページネーションを実際に使う
必要となる実装
追加・変更が必要となる実装は以下です。
- indexビューでページネーションを使うようにする
- Usersコントローラーの
index
アクションの動作を、ページネーションに対応したものに変更する- 具体的には、RDBからユーザーを取得する処理を書き換える
indexページでページネーションを使うようにする
ページネーションのリンクを表示するためのメソッドをapp/views/users/index.html.erb
に追加すればOKです。今回はwill_paginate
メソッドですね。
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
+ <%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
+ <%= will_paginate %>
Railsチュートリアル本文では、indexビュー内のwill_paginate
メソッドの動作について以下のように説明しています。
-
users
ビューのコード中から、@users
オブジェクトを自動的に見つけ出す - 他のページにアクセスするためのページネーションリンクを作成する
will_paginate
をページの上下に2つ追加しているのは、「表示されているユーザー一覧の先頭・末尾両方にページネーションのリンクを表示する」ようにするためです。
will_paginate
メソッドが必要とする@users
変数の内容
will_paginate
メソッドを使う場合、@users
変数はpaginate
メソッドの戻り値である必要があります。paginate
メソッドの実行例を以下に示します。
# rails console --sandbox
>> User.paginate(page: 1)
User Load (14.7ms) SELECT "users".* FROM "users" LIMIT ? OFFSET ? [["LIMIT", 11], ["OFFSET", 0]]
(0.8ms) SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...
-
paginate
メソッドは、キーが:page
で値がページ番号のハッシュを引数として取る -
User.paginate
は、:page
パラメーターに基づき、RDBからUserモデルに紐付けされたひとかたまり(デフォルトでは30)のデータを取り出す- 1ページ目は1番目から31番目のユーザー
- 2ページ目は31番目から60番目のユーザー
- 以下略
- なお、キー
:page
に対する値がnil
の場合は、単に最初のページが返ってくる
# rails console --sandbox
>> User.paginate(page: nil)
User Load (0.2ms) SELECT "users".* FROM "users" LIMIT ? OFFSET ? [["LIMIT", 11], ["OFFSET", 0]]
(0.3ms) SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...
上記はキー:page
に対する値をnil
としてUser.paginate
を実行した例です。
Usersコントローラーのindex
アクションの動作を、ページネーションに対応したものに変更する
具体的には、index
アクション内のall
をpaginate
に置き換えます。なお、params[:page]
は、ビュー側のwill_paginate
によって自動的に与えられます。
def index
@users = User.paginate(page: params[:page])
end
実際にapp/controllers/users_controller.rb
に対して行う変更は以下のとおりになります。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
before_action :correct_user, only: [:edit, :update]
def index
- @users = User.all
+ @users = User.paginate(page: params[:page])
end
...略
end
ページネーション機能が使えるようになる
ここまでの実装が完了すれば、サンプルアプリケーションにおけるページネーション機能が有効になります。

ページネーションのリンクが表示されているのがわかります。

2ページ目は上記スクリーンショットのようになります。
演習 - ページネーション
1. Railsコンソールを開き、page
オプションにnil
をセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。
本文中で例を示したとおりです。
2. 先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.all
のクラスとどこが違うでしょうか? 比較してみてください。
>> User.paginate(page: 1).class
=> User::ActiveRecord_Relation
>> User.paginate(page: 1).class.superclass
=> ActiveRecord::Relation
>> User.paginate(page: 1).class.superclass.superclass
=> Object
>> User.all.class
=> User::ActiveRecord_Relation
paginationオブジェクトのクラスはUser::ActiveRecord_Relation
です。User.all
のクラスと違いはないようです。
ユーザー一覧のテスト
30人超のユーザーをfixtureに追加する
ページネーションに対するテストを行うためには、30人を上回るユーザー情報が必要となります。これだけの人数のユーザー情報を人手で追加するのは面倒です。
しかしながら、fixtureは埋め込みRubyに対応しています。2人のユーザー情報は手動で追加し、さらに30人のそれらしいユーザー情報はイテレータを使って追加することにしましょう。
rhakurei:
name: Reimu Hakurei
email: rhakurei@example.com
password_digest: <%= User.digest('password') %>
mkirisame:
name: Marisa Kirisame
email: example.example@example.org
password_digest: <%= User.digest('password') %>
+
+ skomeiji:
+ name: Satori Komeiji
+ email: example_example@example.net
+ password_digest: <%= User.digest('password') %>
+
+ rusami:
+ name: Renko Usami
+ email: example0@example.com
+ password_digest: <%= User.digest('password') %>
+
+ <% 30.times do |n| %>
+ user_<%= n %>:
+ name: <%= "User #{n}" %>
+ email: <%= "user-#{n}@example.com" %>
+ password_digest: <%= User.digest('password') %>
+ <% end %>
indexに対する統合テストの生成
続いて、indexに対する統合テストを生成します。
# rails generate integration_test users_index
Running via Spring preloader in process 11865
invoke test_unit
create test/integration/users_index_test.rb
indexに対する統合テストを書く
「pagination
クラスを持ったdiv
タグをチェックして、最初のページにユーザーがいることを確認する」というテストを書いていきます。
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@user = users(:rhakurei)
end
test "index including pagination" do
log_in_as(@user)
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
User.paginate(page: 1).each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
end
end
end
テストは無事成功
test/integration/users_index_test.rb
を対象としてテストを実行してみます。
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12058
Started with run options --seed 56737
1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.12612s
1 tests, 32 assertions, 0 failures, 0 errors, 0 skips
無事テストが成功しました。
余談 - 「div.pagination
が無い」と言われてテストが失敗する場合
私はこのテストが通らずに数十分悩むこととなりました。Railsチュートリアル 第10章 - 「div.paginationが無い」と言われてテストが失敗する場合にて、顛末を記述しています。
演習 - ユーザー一覧のテスト
1. 試しにリスト 10.45にあるページネーションのリンク (will_paginate
の部分) を2つともコメントアウトしてみて、リスト 10.48のテストがred
に変わるかどうか確かめてみましょう。
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
- <%= will_paginate %>
+ <%# <%= will_paginate %> %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
- <%= will_paginate %>
+ <%# <%= will_paginate %> %>
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12071
Started with run options --seed 63044
FAIL["test_index_including_pagination", UsersIndexTest, 2.9782001999847125]
test_index_including_pagination#UsersIndexTest (2.98s)
Expected at least 1 element matching "div.pagination", found 0..
Expected 0 to be >= 1.
test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>'
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.98834s
1 tests, 2 assertions, 1 failures, 0 errors, 0 skips
Expected at least 1 element matching "div.pagination", found 0..
というメッセージが出てテストが失敗しています。pagination
というクラスを持つdiv
要素がない、という理由によるテストの失敗ですね。
2.1. 先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが green
のままであることを確認してみましょう。
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
- <%= will_paginate %>
+ <%# <%= will_paginate %> %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
- <%# <%= will_paginate %> %>
+ <%= will_paginate %>
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12084
Started with run options --seed 955
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.81957s
1 tests, 32 assertions, 0 failures, 0 errors, 0 skips
確かにテストが成功しますね。「will_paginate
のリンクは2つ存在しなければテストが失敗する」という実装を実現するために…というのは次の演習です。
2.2. will_paginate
のリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか?
ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。
assert_select 'div.pagination'
に、count: 2
というオプションを追加します。
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
...略
test "index including pagination" do
log_in_as(@user)
get users_path
assert_template 'users/index'
- assert_select 'div.pagination'
+ assert_select 'div.pagination', count: 2
User.paginate(page: 1).each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
end
end
end
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12097
Started with run options --seed 32036
FAIL["test_index_including_pagination", UsersIndexTest, 2.6599518999864813]
test_index_including_pagination#UsersIndexTest (2.66s)
Expected exactly 2 elements matching "div.pagination", found 1..
Expected: 2
Actual: 1
test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>'
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.66190s
1 tests, 2 assertions, 1 failures, 0 errors, 0 skips
Expected exactly 2 elements matching "div.pagination", found 1
というメッセージが出てテストが失敗しています。「pagination
というクラスを持つdiv
要素が2つ必要」と言ってきているので、テスト対象は確かに正しいようです。
app/views/users/index.html.erb
のすべてのコメントアウトを外し、正しい実装コードに戻します。
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
- <%# <%= will_paginate %> %>
+ <%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
- <%# <%= will_paginate %> %>
+ <%= will_paginate %>
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12110
Started with run options --seed 37850
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.50007s
1 tests, 32 assertions, 0 failures, 0 errors, 0 skips
今度こそテストは成功しました。
パーシャルのリファクタリング
動機
indexページに関する機能的実装はここまでで完了し、テストも実装して成功する状態まで持っていくことができました。indexページに関するコードをさらに質の高いコードに書き換えていく準備はここまでで整っている…というのが現状の立ち位置です。
ここまで言及されていませんでしたが、Railsチュートリアル本文によれば、「Railsにはコンパクトなビューを作成するための素晴らしいツールがいくつもある」とのことです。そうしたコードを使ってindexページのコードをリファクタリングしたい、というのがこの節の動機です。
user
パーシャルを実装し、render
で個別ユーザー情報のHTMLコードを生成するようにする
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
- <li>
- <%= gravatar_for user, size: 50 %>
- <%= link_to user.name, user %>
- </li>
+ <%= render user %>
<% end %>
</ul>
<%= will_paginate %>
上記コードのポイントは以下です。
- Userクラスの
user
変数を引数としてrender
を呼び出している -
app/views/users/_user.html.erb
というパーシャルが必要になる
まずはapp/views/users/_user.html.erb
を作成しましょう。
>>> touch app/views/users/_user.html.erb
続いて、app/views/users/_user.html.erb
の内容を記述していきます。
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
ここまでで、「user
パーシャルを実装し、render
で個別ユーザー情報のHTMLコードを生成するようにする」というリファクタリングはひとまず完了しました。
実は、render
は@users
を直接引数に取ることができる
先ほど、「Userクラスのuser
変数を引数としてrender
を呼び出している」と言及しました。実は、@users
を直接引数とし、ブロックを使わない形でrender
を呼び出すというリファクタリングが可能です。
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
<%= will_paginate %>
<ul class="users">
- <% @users.each do |user| %>
- <%= render user %>
- <% end %>
+ <%= render @users %>
</ul>
<%= will_paginate %>
上記コードのポイントは以下です。
- Railsは、
@users
をUserオブジェクトのコレクションであると推測する - Userオブジェクトのコレクションを引数として
render
を呼び出した際、render
の動作は以下となる- Userオブジェクトのコレクションを列挙する
- Userオブジェクトの各インスタンスを、
_user.html.erb
パーシャルで出力する
最後にテストを実行します。
# rails test
Running via Spring preloader in process 12214
Started with run options --seed 3088
39/39: [=================================] 100% Time: 00:00:05, Time: 00:00:05
Finished in 5.56925s
39 tests, 146 assertions, 0 failures, 0 errors, 0 skips
テストは無事成功しました。
演習 - パーシャルのリファクタリング
1. リスト 10.52にあるrenderの行をコメントアウトし、テストの結果がred
に変わることを確認してみましょう。
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
<%= will_paginate %>
<ul class="users">
- <%= render @users %>
+ <%# <%= render @users %> %>
</ul>
<%= will_paginate %>
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12201
Started with run options --seed 4733
FAIL["test_index_including_pagination", UsersIndexTest, 2.5006732000038028]
test_index_including_pagination#UsersIndexTest (2.50s)
Expected at least 1 element matching "a[href="/users/14035331"]", found 0..
Expected 0 to be >= 1.
test/integration/users_index_test.rb:14:in `block (2 levels) in <class:UsersIndexTest>'
test/integration/users_index_test.rb:13:in `block in <class:UsersIndexTest>'
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.51114s
1 tests, 3 assertions, 1 failures, 0 errors, 0 skips
Expected at least 1 element matching "a[href="/users/14035331"]", found 0
というのは、「ユーザー情報へのリンクが描画されていない」という趣旨のメッセージですね。
演習が終わったら、app/views/users/index.html.erb
のコードを元に戻しておくことも忘れずに。
<% provide(:title, 'All Users') %>
<h1>All Users</h1>
<%= will_paginate %>
<ul class="users">
- <%# <%= render @users %> %>
+ <%= render @users %>
</ul>
<%= will_paginate %>