LoginSignup
0
0

More than 3 years have passed since last update.

Railsチュートリアル 第10章 ユーザーの更新・表示・削除 - すべてのユーザーを表示する

Posted at

何をするか

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内です。

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/controllers/users_controller_test.rb(10行目)
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の内容を修正しましょう。

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アクションにに実装する必要があります。

  1. User.allを用い、RDBに保存された全ユーザーの情報を取得する
  2. 取得したユーザー情報を、ビューで使えるユーザー変数@usersに代入する

コードとしては以下のようになります。

@users = User.all

早速Usersコントローラーのindexアクションに反映しましょう。

app/controllers/users_controller.rb
  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を記述していきます。

app/views/users/index.html.erb
<% 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の実装は以下のようになっています。

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に手を加える

app/assets/stylesheets/custom.scss
  ...略
+
+ .users {
+     list-style: none;
+     margin: 0;
+     li {
+         overflow: auto;
+         padding: 10px 0;
+         border-bottom: 1px solid $gray-lighter;
+     }
+ }

  ...略

ビューのヘッダー部に、ユーザー一覧ページへのリンクを追加する

ビューのヘッダー部には、すでにユーザー一覧ページへのリンクが存在します。リンク先は、現時点まで仮に#としてきましたが、ここまでの実装でユーザー一覧ページが表示できるようになったので、実際のユーザー一覧ページにリンクするようにします。

app/views/layouts/_header.html.erb
  <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

新たに実装したテストも含め、すべてのテストが無事通ることが確認できました。

スクリーンショット 2019-11-26 7.40.45.png

上記はログインユーザーで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の変更内容の全体像は以下のようになります。

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を追加する

Gemfile
  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人のユーザー
db/seeds.rb
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ページを表示してみましょう。

スクリーンショット 2019-11-26 18.22.26.png

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_paginatebootstrap-will_paginateの2つになります。早速Gemfileに追加しましょう。

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メソッドですね。

app/views/users/index.html.erb
  <% 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メソッドの動作について以下のように説明しています。

  1. usersビューのコード中から、@usersオブジェクトを自動的に見つけ出す
  2. 他のページにアクセスするためのページネーションリンクを作成する

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アクション内のallpaginateに置き換えます。なお、params[:page]は、ビュー側のwill_paginateによって自動的に与えられます。

def index
  @users = User.paginate(page: params[:page])
end

実際にapp/controllers/users_controller.rbに対して行う変更は以下のとおりになります。

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

ページネーション機能が使えるようになる

ここまでの実装が完了すれば、サンプルアプリケーションにおけるページネーション機能が有効になります。

スクリーンショット 2019-11-27 12.35.46.png

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

スクリーンショット 2019-11-27 12.38.48.png

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人のそれらしいユーザー情報はイテレータを使って追加することにしましょう。

test/fixtures/users.yml
  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タグをチェックして、最初のページにユーザーがいることを確認する」というテストを書いていきます。

test/integration/users_index_test.rb
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に変わるかどうか確かめてみましょう。

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 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のままであることを確認してみましょう。

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 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というオプションを追加します。

test/integration/users_index_test.rb
  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のすべてのコメントアウトを外し、正しい実装コードに戻します。

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コードを生成するようにする

app/views/users/index.html.erb
  <% 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の内容を記述していきます。

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を呼び出すというリファクタリングが可能です。

app/views/users/index.html.erb
  <% 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に変わることを確認してみましょう。

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 %>
# 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のコードを元に戻しておくことも忘れずに。

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 %>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0