LoginSignup
6
4

More than 5 years have passed since last update.

35歳だけどRailsチュートリアルやってみた。[第4版 13章 13.3 マイクロポストを操作する まとめ&解答例]

Posted at

はじめに

最近、プロジェクト管理業務が業務の大半を占めており、
プログラムを書く機会がなかなかありません。

このままだとプログラムがまったく書けない人になってしまう危機感(迫り来る35歳定年説)と、
新しいことに挑戦したいという思いから、
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版を学習中です。
業務で使うのはもっぱらJavaなのですが、Rails楽しいですね。

これまでEvernoteに記録していましたが、ソースコードの貼付けに限界を感じたため、
Qiitaで自分が学習した結果をアウトプットしていきます。

個人の解答例なので、誤りがあればご指摘ください。

動作環境

  • cloud9
  • ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
  • Rails 5.0.0.1

13.3 マイクロポストを操作する

本章での学び

本章では、マイクロポストの作成と削除を行えるようにする。
Micropostsコントローラのアクションは、createとdestroyのみ必要となる。

/sample_app/config/routes.rb
  resources :microposts, only: [:create, :destroy]

routesコマンドで設定値を確認する。

yokoyan:~/workspace/sample_app (user-microposts) $ rails routes
                 Prefix Verb   URI Pattern                                          
・・・略・・・
              microposts POST   /microposts(.:format)                   microposts#create
              micropost DELETE /microposts/:id(.:format)               microposts#destroy

image

13.3.1 マイクロポストのアクセス制御

本章での学び

Micropostsコントローラのcreateアクションや、destroyアクションは、ログイン済みである必要がある。

【test】Micropostsコントローラの認可テスト

  • setup

    • micropostsのfixtureからorangeを取得する
    • インスタンス変数@micropostに代入する
  • should redirect create when not logged in

    • Micropostモデルの数をカウントし、数が変化していないこと
      • POSTリクエストを送信する
        • microposts_path
        • params
          • micropost
            • content
    • ログイン画面にリダイレクトされること
  • should redirect destroy when not logged in

    • Micropostモデルの数をカウントし、数が変化していないこと
      • DELETEリクエストを送信する
    • ログイン画面にリダイレクトされること

上記の仕様で実装する。

/sample_app/test/controllers/microposts_controller_test.rb
  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete micropost_path(@micropost)
    end
    assert_redirected_to login_url
  end

【controller】beforeフィルタのリファクタリング

ユーザーがログインしているかどうかを判定するlogged_in_userメソッドは、Usersコントローラだけでなく、Micropostsコントローラからも使用する。

/sample_app/app/controllers/users_controller.rb
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                    :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

そのため、Usersコントローラ内のlogged_in_userメソッドを、共通の親クラスであるApplicationコントローラに処理を移す。(Javaとは違って、Rubyは継承した親クラス内のprivateメソッドを呼び出せるようだ)

application_controllerにhello, world!のメソッドが残ってる・・・。ここまで長い道のりでした。

/sample_app/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

  def hello
    render html:"hello, world!"
  end

  private

    # ユーザーのログインを確認する
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

リファクタリングが終わったので、Usersコントローラ内のlogged_in_userメソッドは削除する。

【controller】Micropostsコントローラの実装

create、destroyアクションを追加し、前項でリファクタリングしたbeforeフィルタを実装する。

/sample_app/app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
  end

  def destroy
  end
end

【test】テストの実行

この時点でテストがgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 2188
Started with run options --seed 11090

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

Finished in 3.56572s
56 tests, 243 assertions, 0 failures, 0 errors, 0 skips

演習1

なぜUsersコントローラ内にあるlogged_in_userフィルターを残したままにするとマズイのでしょうか? 考えてみてください。

Railsの基本理念であるDRY(Don't repeat yourself)に反するため。
Micropostsコントローラ側でもユーザーのログイン判定を行う必要があり、Usersコントローラ内にlogged_in_userフィルターを残したままにすると、Micropostsコントローラ側にも同じ処理を実装する必要が出てくるため無駄なコードが増えてしまう。

13.3.2 マイクロポストを作成する

本章での学び

【controller】createアクションの実装

Strong parametersで、paramsハッシュでは:micropost属性を必須として、:content属性を許可する。

/sample_app/app/controllers/microposts_controller.rb
  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

ログインしているユーザーのマイクロポストを生成し、DBに保存する。
成功すればroot_urlへリダイレクトする。
失敗すればstatic_pages配下のhomeページを呼び出す。

/sample_app/app/controllers/microposts_controller.rb
  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

【View】Homeページのリファクタリング

ユーザーがログインしているかどうかで表示を変える。

  • ログインしている 
    • user_infoパーシャル(未実装)を表示する
    • micropost_formパーシャル(未実装)を表示する
  • ログインしていない
    • Sign upを促す
/sample_app/app/views/static_pages/home.html.erb
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
<% else %>
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
    This is the home page for the
    <a href="http://railstutorial.jp">Ruby on Rails Tutorial</a>
    sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary"%>
  </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
              'http://rubyonrails.org/'%>
<% end %>

【View】パーシャルの作成

未実装の2つのパーシャルを作成する。

yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/shared/_user_info.html.erb
yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/shared/_micropost_form.html.erb
ユーザー情報パーシャル

Homeページのサイドバーにユーザー情報を表示する。

  • ユーザーのGravatar画像
  • ユーザー名
  • ユーザープロフィール画面へのリンク
  • ユーザーのマイクロポスト数の合計
    • pluralizeヘルパーで英語の複数形に変換
/sample_app/app/views/shared/_user_info.html.erb
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost")%></span>
マイクロポスト投稿フォームパーシャル

マイクロポストを投稿するためのフォーム。

  • form_forで、Micropostモデルに紐づいたフォームを作成する
  • エラーメッセージパーシャルを表示
  • テキストエリアのcontent属性を表示
  • submitボタン
/sample_app/app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>

【controller】StaticPagesコントローラのリファクタリング

homeアクションにマイクロポストのインスタンス変数を定義する。
ログインしているユーザのマイクロポストを取得する。

/sample_app/app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
  def home
    @micropost = current_user.microposts.build if logged_in?
  end

【view】エラーメッセージパーシャルのリファクタリング

form_forで定義する変数が、画面によって変化する。

  • ユーザー登録画面:@user
  • マイクロポスト投稿画面:@micropost

そのため、エラーメッセージパーシャルの呼び出し方法は、
複数のインスタンス変数に対応するためにobject: f.objectとなる。

/sample_app/app/views/shared/_micropost_form.html.erb
<% form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>

そのため、エラーメッセージパーシャル内で
もともと@user固定で呼び出していた箇所を、object変数を使うように
リファクタリングする。

■修正前

/sample_app/app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

■修正後

/sample_app/app/views/shared/_error_messages.html.erb
<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(object.errors.count, "error") %>
    </div>
    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

【test】テスト実行

現時点ではredになってしまう。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 3577
Started with run options --seed 41588

ERROR["test_password_resets", PasswordResetsTest, 2.4003681868780404]
 test_password_resets#PasswordResetsTest (2.40s)
ActionView::Template::Error:         ActionView::Template::Error: undefined local variable or method `object' for #<#<Class:0x007efdd02d7e20>:0x00000005ff8f20>
        Did you mean?  object_id
            app/views/shared/_error_messages.html.erb:1:in `_app_views_shared__error_messages_html_erb__4293521809018927093_50498580'
            app/views/password_resets/edit.html.erb:7:in `block in _app_views_password_resets_edit_html_erb__1534623624385800471_50317940'
            app/views/password_resets/edit.html.erb:6:in `_app_views_password_resets_edit_html_erb__1534623624385800471_50317940'
            test/integration/password_resets_test.rb:40:in `block in <class:PasswordResetsTest>'

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

Finished in 2.83377s
56 tests, 234 assertions, 0 failures, 1 errors, 0 skips

【view】エラーメッセージパーシャルの呼び出し元画面のリファクタリング

1.ユーザー登録画面、ユーザー編集画面

この2画面は共通のフォームを使用しているため、object:@userの箇所を、
object: f.objectにリファクタリングする。

/sample_app/app/views/users/_form.html.erb
    <%= form_for(@user, url: yield(:url)) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>
2.パスワード再登録画面

エラーメッセージパーシャルの呼び出し部分に、object: f.objectを追加する。

/sample_app/app/views/password_resets/edit.html.erb
・・・略・・・
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

【test】再度テストを実行

テスト結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 4197
Started with run options --seed 46999

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

Finished in 3.34028s
56 tests, 243 assertions, 0 failures, 0 errors, 0 skips

ブラウザからアクセスできることを確認。

image

演習1

Homeページをリファクタリングして、if-else文の分岐のそれぞれに対してパーシャルを作ってみましょう。

新規に2つパーシャルを作成する。

yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/static_pages/_home_logged_in.html.erb
yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/static_pages/_home_not_logged_in.html.erb

ログイン時に表示するパーシャル。

/sample_app/app/views/static_pages/_home_logged_in.html.erb
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>

未ログイン時に表示するパーシャル。

/sample_app/app/views/static_pages/_home_not_logged_in.html.erb
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
    This is the home page for the
    <a href="http://railstutorial.jp">Ruby on Rails Tutorial</a>
    sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary"%>
  </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
              'http://rubyonrails.org/'%>

リファクタリング後のhomeページ。
かなりスッキリしました。

/sample_app/app/views/static_pages/home.html.erb
<% if logged_in? %>
  <%= render 'static_pages/home_logged_in' %>
<% else %>
  <%= render 'static_pages/home_not_logged_in' %>
<% end %>

リファクタリング後もtestがgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test 
Running via Spring preloader in process 5658
Started with run options --seed 10843

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

Finished in 3.49162s
56 tests, 243 assertions, 0 failures, 0 errors, 0 skips

13.3.3 フィードの原型

本章での学び

ユーザープロフィール画面に、ユーザーのマイクロポストのフィードを追加する。

【model】Userモデルにfeedメソッドを実装する

すべてのユーザーがフィードを持つため、Userモデルにfeedメソッドを追加する。
SQLインジェクション対策のために、プリペアドステートメントを使う。
(SQLのwhere句にuser.idが入る際にエスケープされる)

/sample_app/app/models/user.rb
  # 試作feedの定義
  # 完全な実装は次章の「ユーザーをフォローする」を参照
  def feed
    Micropost.where("user_id = ?", id)
  end

【controller】static_pagesコントローラのリファクタリング

homeアクションのリファクタリングを行う。
1行だった後置if文を、前置if文に変更する。
また、前項で実装したfeedメソッドの取得結果をインスタンス変数@feed_itemsに格納する。

/sample_app/app/controllers/static_pages_controller.rb
  def home
    if logged_in?
      @micropost = current_user.microposts.build if logged_in?
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end

【view】フィード表示パーシャルを作成する

インスタンス変数@feed_itemsを表示するためのパーシャルを作成する。
@feed_items内の要素は、Micropostクラスを保持しているため、Micropostのパーシャルを呼び出すことができる。
(app/view/microposts/_micropost.html.erb)

yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/shared/_feed.html.erb
/sample_app/app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
    <%= will_paginate @feed_items %>
<% end %>

【view】homeページヘの組み込み

13.3.2の演習で作成したパーシャルから、フィード表示パーシャルの呼び出しを行う。

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

動作確認

追加したフィードが表示されていることを確認。

image

ただし、マイクロポストの投稿に失敗した場合は、エラーが発生する。
@feed_itemsを期待しているが取得できていない)

image

対策として、DB保存失敗時には空の配列で@feed_itemsを定義する。

/sample_app/app/controllers/microposts_controller.rb
  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end

DB保存失敗時にエラーが発生しないことを確認。

image

演習1

新しく実装したマイクロポストの投稿フォームを使って、実際にマイクロポストを投稿してみましょう。Railsサーバーのログ内にあるINSERT文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。

投稿フォームからマイクロポストを投稿する。

image

ログは以下の通り。
INSERT文もプリペアドステートメントになっている。

Started POST "/microposts" for 121.119.136.219 at 2017-08-19 05:37:49 +0000
Cannot render console from 121.119.136.219! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"qHHNxLqH2NJGPTtIdVD5Q4qaNFZXQVt9qkT0D3caNiy5wZ5QD4JaK/VGomjss64pq7nSXcneQXcPCehIT5W3rQ==", "micropost"=>{"content"=>"投稿テスト1行目\r\n投稿テスト2行目"}, "commit"=>"Post"}
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "投稿テスト1行目\r\n投稿テスト2行目"], ["user_id", 1], ["created_at", 2017-08-19 05:37:49 UTC], ["updated_at", 2017-08-19 05:37:49 UTC]]
   (10.9ms)  commit transaction
Redirected to https://rails-tutorial-yokoyan.c9users.io/
Completed 302 Found in 23ms (ActiveRecord: 11.9ms)

演習2

コンソールを開き、user変数にデータベース上の最初のユーザーを代入してみましょう。その後、Micropost.where("user_id = ?", user.id)とuser.microposts、そしてuser.feedをそれぞれ実行してみて、実行結果がすべて同じであることを確認してみてください。ヒント: ==で比較すると結果が同じかどうか簡単に判別できます。

実行結果は以下の通り。
すべて同じであることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails console --sandbox
Running via Spring preloader in process 6116
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>> 
?> user = User.first
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>
>> 
?> Micropost.where("user_id = ?", user.id) == user.microposts
  Micropost Load (0.6ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC  [["user_id", 1]]
  Micropost Load (0.5ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC
=> true
>> 
?> Micropost.where("user_id = ?", user.id) == user.feed
=> true

13.3.4 マイクロポストを削除する

本章での学び

本章では、マイクロポストの削除機能を実装する。

【view】マイクロポストのパーシャルのリファクタリング

削除リンクを追加する。
自分が投稿したマイクロポストのみ削除することができる。

/sample_app/app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                        data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>

【controller】Micropostsコントローラのdestroyアクションを実装する

request.referrerメソッドを使って、1つ前のページヘリダイレクトする。
リファラーが取得できない場合は、root_urlへリダイレクトする。

/sample_app/app/controllers/microposts_controller.rb
  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
  end```

####【controller】beforeフィルタを追加する
destroyアクション実施前実行するbeforeフィルタを追加する。
ログインしているユーザのマイクロポストが存在しない場合は、root_urlへリダイレクトする。

```/sample_app/app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user, only: [:destroy]
・・・略・・・
  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

    def correct_user
      @micropost = current_user.micropost.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end

演習1

マイクロポストを作成し、その後、作成したマイクロポストを削除してみましょう。次に、Railsサーバーのログを見てみて、DELETE文の内容を確認してみてください。

ブラウザからアクセスし、任意のマイクロポストが削除できることを確認。

image

ログは以下の通り。

Started DELETE "/microposts/302" for 121.119.136.219 at 2017-08-19 07:14:05 +0000
Cannot render console from 121.119.136.219! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#destroy as HTML
  Parameters: {"authenticity_token"=>"IdXOzp0PdnF6givkKNnhrUQrfoTz5WNa3X9ZQHAbZU/nu0rzvvRjbr0Iix4fPzhqzmx435NnWbNhAk0Oz6TcOw==", "id"=>"302"}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? ORDER BY "microposts"."created_a " DESC LIMIT ?  [["user_id", 1], ["id", 302], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (108.7ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 302]]
   (17.0ms)  commit transaction
Redirected to https://rails-tutorial-yokoyan.c9users.io/
Completed 302 Found in 168ms (ActiveRecord: 128.5ms)

演習2

redirect_to request.referrer || root_urlの行をredirect_back(fallback_location: root_url)と置き換えてもうまく動くことを、ブラウザを使って確認してみましょう (このメソッドはRails 5から新たに導入されました)。

確かにGithubでも、redirect_backメソッドを追加したって言ってます。
redirect_to :backだと、完全に安全ではなく、
リクエストで利用できるリファラーがない場合、ActionController::RedirectBackErrorが発生するとのこと。

Add redirect_back and deprecate redirect_to :back #22506

以下の記事もわかりやすかったです。

Rails 5.1系からは redirect_to :backがきえまっせ

ということで、rediret_backメソッドを使うようにリファクタリング。

/sample_app/app/controllers/microposts_controller.rb
  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    # redirect_to request.referrer || root_url
    redirect_back(fallback_location: root_url)
  end

問題なく動くことを確認。
新しいAPI使うほうがいいですね!

image

13.3.5 フィード画面のマイクロポストをテストする

本章での学び

作成したマイクロポストの単体テストと統合テストを作成する。

【事前準備】テストデータの用意

マイクロポストのfixtureにデータを追加する。

/sample_app/test/fixtures/microposts.yml
・・・略・・・
ants:
  content: "Oh, is that what you want? Because that's how you get ants!"
  created_at: <%= 2.years.ago %>
  user: archer

zone:
  content: "Danger zone!"
  created_at: <%= 3.days.ago %>
  user: archer

tone:
  content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
  created_at: <%= 10.minutes.ago %>
  user: lana

van:
  content: "Dude, this van's, like, rolling probable cause."
  created_at: <%= 4.hours.ago %>
  user: lana

【test】テストコードの作成

マイクロポストのテストコードを作成する。
自分のマイクロポストではないものを削除して

  • should redirect destroy for wrong micropost
    • michaelユーザでログインする
    • マイクロポストのfixtureのantsを取得する(archerユーザのマイクロポスト)
    • マイクロポストの件数が変わらないことを確認
      • マイクロポストを削除する
    • root_urlにリダイレクトすることを確認
/sample_app/test/controllers/microposts_controller_test.rb
  test "should redirect destroy for wrong micropost" do
    log_in_as(users(:michael))
    micropost = microposts(:ants)
    assert_no_difference 'Micropost.count' do
      delete micropost_path(micropost)
    end
    assert_redirected_to root_url
  end

実行結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 7833
Started with run options --seed 65115

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

Finished in 3.68433s
57 tests, 245 assertions, 0 failures, 0 errors, 0 skips

【test】マイクロポストのUIに関する統合テストを作成する

rails generateで自動生成する。

yokoyan:~/workspace/sample_app (user-microposts) $ rails generate integration_test microposts_interface
Running via Spring preloader in process 7975
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  test_unit
      create    test/integration/microposts_interface_test.rb
  • setup

    • michaelユーザの情報を取得する
    • @userに代入する
  • micropost interface

    • ログインする
    • root_pathへ、getリクエストを送信する
    • HTMLに div class=pagination要素があることを確認
    • マイクロポストの数が変わらないことを確認
      • microposts_pathへpostリクエストを送信
        • params
          • micropost
          • content属性は空
    • HTMLにdiv id=error_explanation要素があることを確認
    • 変数contentに文字列を代入
    • マイクロポストの数が変わることを確認
      • microposts_pathへpostリクエストを送信
        • params
          • micropost
          • content属性は代入した変数content
    • root_urlへリダイレクトされること
    • リダイレクトを追う
    • 代入したcontentの文字列が、HTML内に一致すること
    • HTML内にdeleteテキストとaタグがあることを確認
    • ユーザの1番目のマイクロポストを取得する
    • マイクロポストの数が-1されることを確認
      • micropost_pathへdeleteリクエストを送信
    • archerユーザのプロフィールページヘgetリクエストを送信
    • HTML内にdeleteテキストとaタグが0件であることを確認

上記を踏まえて実装する。

/sample_app/test/integration/microposts_interface_test.rb
  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    # 無効な送信
    assert_no_difference 'Micropost.count' do
      post microposts_path params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    assert_difference 'Micropost.count', 1 do
      post microposts_path params: { micropost: { content: content } }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    # 投稿を削除する
    assert_select 'a', text: 'delete'
    micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(micropost)
    end
    # 違うユーザーのプロフィールにアクセス(削除リンクがないことを確認)
    get user_path(users(:archer))
    assert_select 'a', text: 'delete', count: 0
  end

実行結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 2271
Started with run options --seed 12967

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

Finished in 3.92997s
58 tests, 255 assertions, 0 failures, 0 errors, 0 skips

演習1

リスト 13.55で示した4つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが redになることを確認し、元に戻すと greenになることを確認してみましょう。

「無効な送信」のテスト

createメソッドをコメントアウトする。

/sample_app/app/controllers/microposts_controller.rb
  # def create
  #   @micropost = current_user.microposts.build(micropost_params)
  #   if @micropost.save
  #     flash[:success] = "Micropost created!"
  #     redirect_to root_url
  #   else
  #     @feed_items = []
  #     render 'static_pages/home'
  #   end
  # end

16行目でエラーになることを確認。

ERROR["test_micropost_interface", MicropostsInterfaceTest, 3.938271726015955]
 test_micropost_interface#MicropostsInterfaceTest (3.94s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'create' could not be found for MicropostsController
            test/integration/microposts_interface_test.rb:17:in `block (2 levels) in <class:MicropostsInterfaceTest>'
            test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

「有効な送信」のテスト

DB保存に成功した際の処理をコメントアウトする。

/sample_app/app/controllers/microposts_controller.rb
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      # flash[:success] = "Micropost created!"
      # redirect_to root_url
    else

25行目でエラーになることを確認。

FAIL["test_micropost_interface", MicropostsInterfaceTest, 2.1839206719305366]
 test_micropost_interface#MicropostsInterfaceTest (2.18s)
        Expected response to be a <3XX: redirect>, but was a <204: No Content>
        test/integration/microposts_interface_test.rb:25:in `block in <class:MicropostsInterfaceTest>'

「投稿を削除する」のテスト

deleteリンクを表示させないために、@feed_itemsの描画をコメントアウトする。

/sample_app/app/views/microposts/_micropost.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <!--<%= render @feed_items %>-->
  </ol>

29行目でエラーになることを確認。

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 1.6857899979222566]
 test_micropost_interface#MicropostsInterfaceTest (1.69s)
        <delete> expected but was
        <sample app>..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:29:in `block in <class:MicropostsInterfaceTest>'

「違うユーザーのプロフィールにアクセス」のテスト

ログインユーザーとマイクロポストのユーザーが一致するかの判定をコメントアウト。

/sample_app/app/views/microposts/_micropost.html.erb
    <% #if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                        data: { confirm: "You sure?" } %>
    <% #end %>

36行目でエラーになることを確認。

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 2.53247292409651]
 test_micropost_interface#MicropostsInterfaceTest (2.53s)
        Expected exactly 0 elements matching "a", found 2..
        Expected: 0
          Actual: 2
        test/integration/microposts_interface_test.rb:36:in `block in <class:MicropostsInterfaceTest>'

演習2

サイドバーにあるマイクロポストの合計投稿数をテストしてみましょう。このとき、単数形 (micropost) と複数形 (microposts) が正しく表示されているかどうかもテストしてください。ヒント: リスト 13.57を参考にしてみてください。

  • micropost sidebar count
    • michaelユーザでログイン
    • root_pathへgetリクエスト送信
    • michaelユーザのマイクロポスト数をカウントし、複数形で表示されることを確認
    • 別ユーザ(maloryユーザ)でログイン
    • root_pathへgetリクエスト送信
    • HTMLに"0 micropost"と単数形で表示されることを確認
    • maloryユーザでマイクロポストを投稿する
    • root_pathへgetリクエスト送信
    • HTMLに"1 micropost"と単数形で表示されることを確認

上記を踏まえて実装する。

/sample_app/test/integration/microposts_interface_test.rb
  test "micropost sidebar count" do
    log_in_as(@user)
    get root_path
    assert_match "#{ @user.microposts.count } microposts", response.body
    # まだマイクロポストを投稿していないユーザー
    other_user = users(:malory)
    log_in_as(other_user)
    get root_path
    assert_match "0 microposts", response.body
    other_user.microposts.create!(content: "A micropost")
    get root_path
    assert_match "1 micropost", response.body
  end

おわりに

マイクロポストの投稿から、フィード表示、削除までマイクロポストの主要機能が実装できました。
本章はボリュームがあってしんどかったですが、
ユーザーのCRUD処理と似ていたので、理解しやすかったです。

6
4
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
6
4