0
0

More than 3 years have passed since last update.

Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストの作成・削除に関する機能を、テスト駆動開発で実装する

Last updated at Posted at 2020-01-03

別のユーザーに所属するマイクロポストをfixtureに追加する

今後のテストで使うために、マイクロポストのfixtureに対して、最初のテストで使ったユーザーとは別のユーザーに属するマイクロポストを追加していきます。

test/fixtures/microposts.yml
  ...略
+
+ ants:
+   content: "Oh, Is that what you want? Because that's haw you get ants!"
+   created_at: <%= 2.years.ago %>
+   user: :mkirisame
+
+ zone:
+   content: "Danger zone!"
+   created_at: <%= 3.days.ago %>
+   user: :mkirisame
+
+ tone:
+   content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
+   created_at: <%= 10 minutes.ago %>
+   user: :rusami
+
+ van:
+   content: "Dude, this van's, like, rolling probable cause."
+   created_at: <%= 4.hours.ago %>
+   user: :rusami

マイクロポストのUIに対する統合テストを追加する

マイクロポストのUIに対する統合テストの生成

例によってrails generate integration_testコマンドによってテストを生成していきます。テストの名前はmicroposts_interface_testとします。

# rails generate integration_test microposts_interface
Running via Spring preloader in process 2380
      invoke  test_unit
      create    test/integration/microposts_interface_test.rb

マイクロポストのUIに対する統合テストの実装

マイクロポストのUIに対する統合テストの全体像は、以下のようになります。先程生成したばかりのtest/integration/microposts_interface_test.rbにコードを記述していきます。

test/integration/microposts_interface_test.rb
require 'test_helper'

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

  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'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # 違うユーザーのプロフィールにアクセス(削除リンクがないことの確認)
    get user_path(users(:mkirisame))
    assert_select 'a', text: 'delete', count: 0
  end
end

テストの内容は以下になります。

  • Homeページに、マイクロポストを表示する部分が実装されていること
  • 内容が空のマイクロポストがMicropostsリソースにPOSTされた際に、RDBにマイクロポストが保存されないこと
    • その際、エラーメッセージがerror_explanation属性を持つdiv要素としてレンダリングされていること
  • 有効な内容のマイクロポストがMicropostsリソースにPOSTされた際に、RDBにマイクロポストが保存されること
    • その際、Homeページにリダイレクトされること
    • リダイレクト後のHomeページに、当該マイクロポストの内容がレンダリングされていること
  • 「delete」という文字を内容とするa要素が存在すること
    • マイクロポストの削除リンクを表す
  • Micropostsリソースが正しいDELETEリクエストを受け取った際に、RDBから対象のマイクロポストが削除されること
    • 削除されるマイクロポストは、setupメソッド中で指定したユーザーの最新のマイクロポスト
  • ログインユーザーとは違うユーザーのプロフィールにアクセスした際に、「delete」という文字を内容とするa要素が存在しないこと

マイクロポストのUIに対する統合テスト実装時点でのテストの実行結果

上記コードを保存した時点でtest/integration/microposts_interface_test.rbを対象にテストを行うと、結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2401
Started with run options --seed 51205

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.5630840999947395]
 test_micropost_interface#MicropostsInterfaceTest (3.56s)
        Expected at least 1 element matching "div.pagination", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:11:in `block in <class:MicropostsInterfaceTest>'

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

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

以下のようなメッセージが出て、テストが失敗しています。

Expected at least 1 element matching "div.pagination", found 0..
Expected 0 to be >= 1.
test/integration/microposts_interface_test.rb:11

私の環境では、test/integration/microposts_interface_test.rbの11行目には以下のコードが記述されています。

test/integration/microposts_interface_test.rb(11行目)
assert_select 'div.pagination'

まずはHomeページにマイクロポストのフィードを表示する部分を実装する必要がありそうですね。

Homeページにマイクロポストのフィードを表示する部分を実装する

Railsチュートリアル本文とは順番が前後しますが、テストの失敗内容を考えると、先に実装するのは「Homeページにマイクロポストのフィードを表示する部分」となります。

Railsチュートリアル本文には、「マイクロポスト投稿フォームと、マイクロポストのフィード表示部分が実装されたHomeページのモックアップ」が、図 13.13として示されています。

現在ログインしているユーザーのマイクロポストを全て取得する

ログイン済みユーザーのHomeページに表示するマイクロポストのフィードの内容は、「当該ユーザーが投稿したマイクロポスト」となります。このことは全てのユーザーに共通します。

「フィードの内容を取得する」という処理の内容は、全てのユーザーに共通する内容です。そのため、当該処理はUserモデルに実装するのが自然な流れとなります。編集対象のファイルはapp/models/user.rbですね。

また、当該メソッドの名前はfeedとします。

feedメソッドの内容

feedメソッドの内容は、現在のところは、「whereメソッドにより、ログインユーザー自身の全てのマイクロポストを取得する」というものにしておきます。

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

なお、Railsチュートリアル本文では、今後「指定したユーザーの全てのマイクロポストを取得する」という内容にしていくことが示唆されています。

whereメソッドの第1引数中で使われている?の意味

Micropost.where("User.id = ?", id)

上記whereメソッドの第1引数で使われている?は、「SQLクエリを組み立てる際、引数中の危険な文字をエスケープする処理を行わせる」という動作を意味します。これは、「SQLインジェクション」という深刻なセキュリティホールを回避するために非常に重要な実装となります。Railsに限らず、SQL文に変数を代入する処理を記述する際には、常にエスケープ処理とセットで記述することが強く望まれます。

あえてUser.idを与えるような実装にした理由

「ログインユーザー自身の全てのマイクロポストを取得する」のみであれば、以下の実装内容でも実現可能です。

def feed
  microposts
end

にもかかわらず、あえてUser.idを与えるような実装にしたのは、今後「指定したユーザーの全てのマイクロポストを取得する」という実装内容にしていくことを想定したためです。

Userモデルに、実際にfeedメソッドを実装する

上記の内容を踏まえて、app/models/user.rbに対しては以下の変更を反映していきます。

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

    private

      ...略
  end

homeアクションにフィードのインスタンス変数を追加する

Home画面にフィードを表示させるには、StaticPagesコントローラーのhomeメソッドに、フィード内容を与えるためのインスタンス変数を定義する必要があります。名前は@feed_itemsとしましょう。

@feed_itemsの定義は、さしあたって以下のようになります。

@feed_items = current_user.feed.paginate(page: params[:page]) if logged_in?

app/controllers/static_pages_controller.rbの内容は、さしあたって以下のように変更します。

app/controllers/static_pages_controller.rb
  class StaticPagesController < ApplicationController
    def home
+     @feed_items = current_user.feed.paginate(page: params[:page]) if logged_in?
    end

    def help
    end

    def about
    end

    def contact
    end
  end

ステータスフィードのパーシャル

Homeビューにステータスフィードを表示させるためのパーシャルも必要になります。パーシャルそのものの名前はfeedとします。対応するファイル名はapp/views/shared/_feed.html.erbとなります。

app/views/shared/_feed.html.erbの内容は以下のようになります。

app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

ステータスフィードのパーシャルからでも、単一のマイクロポストを表示するパーシャルを使うことができる

Railsチュートリアル本文においては、「ステータスフィードのパーシャルは、単一のマイクロポストを表示するパーシャルとは内容が異なる」という点が強調されています。

<%= render @feed_items %>

@feed_itemsを構成する個々の要素(より具体的には、前述feedメソッドの個々の戻り値)は、Micropostクラスのインスタンスです。このような場合においてRailsは、自身の機能により、暗黙的にMicropostのパーシャルを呼び出すことが可能なのです。

以下に、Micropostのパーシャルの実体であるapp/views/microposts/_micropost.html.erbを再掲しておきます。

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.
  </span>
</li>

Micropostに限らず、Railsのビューに対してリソースが渡された場合、Railsは対応する名前のパーシャルを、渡されたリソースのディレクトリ内から探し出すことができます。

ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる

Homeページにステータスフィードを実装する前提条件として、「ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる」という処理を実装する必要があります。

ここまでHomeページに実装してきたサインアップページヘのリンクは、ログインしていない場合にのみ必要となる内容です。ログインしている場合に対するHomeページの内容は、また別に定義する必要があります。

サインアップページへのリンクを、ログインしていない場合にのみ表示する

まずは「サインアップページへのリンクを、ログインしていない場合にのみ表示する」という実装内容を反映していきましょう。少々コードが汚いですが、ひとまずif-elseで分岐を行うようにします。

対象のファイルはapp/views/static_pages/home.html.erbですね。

app/views/static_pages/home.html.erb
  <% provide(:title, "Home") %>
  <% if logged_in? %>
    <%#TODO: ログイン済みユーザーに対する処理の実装 %>
  <% else %>
    <div class="center jumbotron">
      <h1>Welcome to the Sample App</h1>

      <h2>
        This is the home page for the
        <a href="https://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 %>

Homeページにステータスフィードを追加する

  • Userモデルにおいて、ログインユーザー自身の全マイクロポストをRDBから取得する機能
  • StaticPagesコントローラーのhomeアクションにおける、フィード内容を中身とするインスタンス変数の定義
  • ステータスフィードのパーシャル
  • ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる機能

以上の実装が完成すれば、Homeページにステータスフィードを追加することができるようになります。

では、Homeパージにステータスフィードを追加していきます。

  <% provide(:title, "Home") %>
  <% if logged_in? %>
-   <%#TODO: ログイン済みユーザーに対する処理の実装 %>
+   <div class="row">
+     <aside class="col-md-4">
+       <%#TODO: ユーザー情報表示部の実装 %>
+       <%#TODO: マイクロポスト投稿フォームの実装 %>
+     </aside>
+     <div class="col-md-8">
+       <h3>Micropost Feed</h3>
+       <%= render 'shared/feed' %>
+     </div>
+   </div>
  <% else %>
    <div class="center jumbotron">
      <h1>Welcome to the Sample App</h1>

      <h2>
        This is the home page for the
        <a href="https://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 %>

これでHomeページにステータスフィードが追加できたはずです。

Homeページへのステータスフィードの追加が完了した時点でのテストの実行結果

Homeページへのステータスフィードの追加が完了した時点で、test/integration/microposts_interface_test.rbを対象としたテストを実行すると、結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2446
Started with run options --seed 43986

ERROR["test_micropost_interface", MicropostsInterfaceTest, 3.7222297999833245]
 test_micropost_interface#MicropostsInterfaceTest (3.72s)
NoMethodError:         NoMethodError: undefined method `document' for nil:NilClass
            test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

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

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

私の環境では、test/integration/microposts_interface_test.rbの13行目から16行目の内容は以下のようになっています。

test/integration/microposts_interface_test.rb(13〜16行目)
# 無効な送信
assert_no_difference 'Micropost.count' do
  post microposts_path, params: { micropost: { content: "" } }
end
assert_select 'div#error_explanation'

エラーメッセージの内容は以下のようになっています。

NoMethodError: undefined method `document' for nil:NilClass

「無効なマイクロポストのPOSTに対し、有効なオブジェクトを返してきていない」という趣旨のエラーと考えられます。まずは「無効なマイクロポストのPOSTに対する処理」を実装する必要がありそうですね。

マイクロポスト作成機能の実装についての概要

「無効なマイクロポストのPOSTに対する処理」という言及を先にしてしまいましたが、まずはマイクロポスト作成機能の実装そのものについての説明が必要ですね。

「マイクロポストを作成する」という処理は、「Micropostsコントローラーに対するHTTPのPOSTリクエストの発行」をトリガーとして行われます。「HTTPのPOSTリクエストを、コントローラーのcreateアクションに向けて発行する」という流れそのものは、過去に実装した「ユーザーのサインアップ」と類似しています。

無効なマイクロポストのPOSTに対する処理

Micropostsコントローラーのcreateアクション

マイクロポストの投稿に際し、Webブラウザで入力する必要十分なパラメーターをStrong Parametersに設定する

マイクロポストの投稿に際し、Webブラウザのフォームで入力する必要があるパラメーターは、マイクロポストのcontent属性のみです。それ以外のパラメーターは、逆にPOSTリクエストから変更可能であってはなりません。

Micropostコントローラーのcreateアクションでは、「Webブラウザから送信されるPOSTリクエストのうち、content属性のみを受け取るようにし、それ以外の属性は受け取らないようにする」という実装が必要になります。ここで使うのはStrong Parameters機能ですね。

privateメソッド以降にmicropost_paramsというメソッドを定義し、Strong Parameters機能の使用に必要な実装を行っていきます。

private

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

micropost_paramsメソッドでは、以下の条件について記述しています。

  • POSTリクエストのクエリパラメータにはmicropostパラメータを必要とする
  • POSTリクエストのmicropostパラメータのうち、contentパラメータのみを受理する

RDBに保存されるマイクロポストを生成する

@micropost = current_user.microposts.build(micropost_params)

上記コードにより、ログイン済みのユーザーに紐付けられた新たなマイクロポストを生成し、インスタンス変数@micropostに格納します。micropost_paramsというのは、前の項で記述した通り、「POSTリクエストに必要なパラメータと、POSTリクエストから受理するパラメータ」について記述するメソッドとなります。

無効なマイクロポストがPOSTされた場合の処理

ここまで言及した内容により、ひとまず「無効なマイクロポストがPOSTされた場合の処理」を記述していくことができるようになります。対象としたapp/controllers/microposts_controller.rbです。

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

    def create
+     @micropost = current_user.microposts.build(micropost_params)
+     if @micropost.save
+       #TODO: ここに有効なマイクロポストの投稿に対する処理を書く
+     else
+       render 'static_pages/home'
+     end
    end

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

上記実装が完了した時点でのテストの実行結果

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2472
Started with run options --seed 5099

ERROR["test_micropost_interface", MicropostsInterfaceTest, 3.8275596000021324]
 test_micropost_interface#MicropostsInterfaceTest (3.83s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `any?' for nil:NilClass
            app/views/shared/_feed.html.erb:1:in `_app_views_shared__feed_html_erb__1031165391329753733_47227964624820'
            app/views/static_pages/home.html.erb:10:in `_app_views_static_pages_home_html_erb__2360347270319602419_47227964499260'
            app/controllers/microposts_controller.rb:9:in `create'
            test/integration/microposts_interface_test.rb:14:in `block (2 levels) in <class:MicropostsInterfaceTest>'
            test/integration/microposts_interface_test.rb:13:in `block in <class:MicropostsInterfaceTest>'

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

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

何か別のところでエラーを返すようになりましたね。

どのようなプロセスでエラーに至るのか見当がつかないので、debuggerメソッドを挿入して確認してみましょう。

...略
[5, 14] in /var/www/sample_app/app/controllers/microposts_controller.rb
    5:     @micropost = current_user.microposts.build(micropost_params)
    6:     if @micropost.save
    7:       #TODO: ここに有効なマイクロポストの投稿に対する処理を書く
    8:     else
    9:       debugger
=> 10:       render 'static_pages/home'
   11:     end
   12:   end
   13: 
   14:   def destroy
(byebug) 
[1, 7] in /var/www/sample_app/app/views/shared/_feed.html.erb
   1: <% debugger %>
=> 2: <% if @feed_items.any? %>
   3:   <ol class="microposts">
   4:     <%= render @feed_items %>
   5:   </ol>
   6:   <%= will_paginate @feed_items %>
   7: <% end %>
(byebug) @feed_items
nil

「マイクロポストの投稿が失敗し、static_pages/homeがレンダリングされる時点で@feed_itemsnilになってしまうこと」がエラーの原因のようですね。

マイクロポストの投稿が失敗した場合に、@feed_itemsに空の配列を渡すようにする

Railsチュートリアル本文には、上記動作について、以下のように記述されています。

ただしささいなことではありますが、マイクロポストの投稿が失敗すると、 Homeページは@feed_itemsインスタンス変数を期待しているため、現状では壊れてしまいます。最も簡単な解決方法は、リスト 13.50のように空の配列を渡しておくことです。

実際にそのように実装してみましょう。

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

    def create
      @micropost = current_user.microposts.build(micropost_params)
      if @micropost.save
        #TODO: ここに有効なマイクロポストの投稿に対する処理を書く
      else
+       @feed_items = []
        render 'static_pages/home'
      end
    end

    ...略
  end

マイクロポストの投稿が失敗した場合に、@feed_itemsに空の配列を渡すようにした時点でのテストの結果

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2573
Started with run options --seed 310

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.6217254000075627]
 test_micropost_interface#MicropostsInterfaceTest (3.62s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 3.62327s
1 tests, 3 assertions, 1 failures, 0 errors, 0 skips

error_explanationクラスを持つdiv要素が見つからないというメッセージを出力してテストが失敗していますね。どうやらstatic_pages/homeのレンダリングまではうまくいっているようです。今度は、「static_pages/homeerror_explanationクラスを持つdiv要素がレンダリングされるようにする」必要があります。

マイクロポスト投稿フォームの仮パーシャルを追加する

Railsチュートリアル本文中の記載に従えば、ログイン済みユーザーのマイクロポスト表示画面において「error_explanationクラスを持つdiv要素」がレンダリングされるのは、マイクロポスト投稿フォーム内となります。まずは、マイクロポスト投稿フォームとなる予定の仮パーシャルを作成し、そこに「error_explanationクラスを持つdiv要素」がレンダリングされるようにしましょう。

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

@micropostというインスタンス変数が出てきました。パーシャルapp/views/shared/_micropost_form.html.erbを使うのはHomeページなので、StaticPagesコントローラーのhomeアクションにも@micropostが必要になります。

StaticPagesコントローラーのhomeアクションに、インスタンス変数@micropostの定義を追加する

StaticPagesコントローラーのhomeアクションにおける@micropostの定義は以下になります。

@micropost = current_user.microposts.build if logged_in?

結果、StaticPagesコントローラーのhomeアクションの新たな定義は以下のようになります。

  class StaticPagesController < ApplicationController
    def home
-     @feed_items = current_user.feed.paginate(page: params[:page]) if logged_in?
+     if logged_in?
+       @micropost  = current_user.microposts.build
+       @feed_items = current_user.feed.paginate(page: params[:page])
+     end
    end

    def help
    end

    def about
    end

    def contact
    end
  end

logged_in?が真であることを条件とするメソッド呼び出しが2つになったので、if logged_in?を後置ifから前置ifに変更しています。

マイクロポスト投稿フォームの仮パーシャルをHomeページで使うようにする

今作成したマイクロポスト投稿フォームの仮パーシャルは、Homeページで使うものです。app/views/static_pages/home.html.erbの内容も、当該パーシャルを使用するように変更する必要があります。変更内容は以下の通りです。

app/views/static_pages/home.html.erb
  <% provide(:title, "Home") %>
  <% if logged_in? %>
    <div class="row">
      <aside class="col-md-4">
        <%#TODO: ユーザー情報表示部の実装 %>
-       <%#TODO: マイクロポスト投稿フォームの実装 %>
+       <section class="micropost_form">
+         <%= render 'shared/micropost_form' %>
+       </section>
      </aside>
      <div class="col-md-8">
        <h3>Micropost Feed</h3>
        <%= render 'shared/feed' %>
      </div>
  <% else %>
    <div class="center jumbotron">
      <h1>Welcome to the Sample App</h1>

      <h2>
        This is the home page for the
        <a href="https://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 %>

エラーメッセージのパーシャルを再定義する

新たに必要となる実装

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

このコードは、エラーメッセージのパーシャルであるapp/views/shared/_error_messages.html.erbの内容が現状のままでは正常に動作しません。理由は以下です。

  • 現状のapp/views/shared/_error_messages.html.erbでは、内部で使うインスタンス変数が@user決め打ちである
  • 一方で、app/views/shared/_micropost_form.html.erbから渡されるインスタンス変数は@micropostである

app/views/shared/_error_messages.html.erb@user@micropostも受け取ることができるような実装に変えねばなりません。

app/views/shared/_error_messages.html.erbを、@user@micropostも受け取ることができるような実装に変更する

renderメソッドのオプションとして、以下のハッシュを与えているのがポイントです。

object: f.object

上記は「パーシャル中で使う変数名をキーとし、当該変数の内容となるオブジェクトを値とするハッシュ」です。例えば以上のハッシュをrenderメソッドのオプションとして与えた場合、レンダリングされるパーシャル(今回はerror_messagesパーシャル)にobjectという変数名でf.objectの内容を使うことができるようになります。

app/views/shared/_error_messages.html.erbの変更内容は以下の通りになります。

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

エラーメッセージのパーシャルを書き換えた時点でのテストの実行結果

# rails test
Running via Spring preloader in process 2599
Started with run options --seed 63753

ERROR["test_password_resets", PasswordResetsTest, 3.290402600017842]
 test_password_resets#PasswordResetsTest (3.29s)
ActionView::Template::Error:         ActionView::Template::Error: undefined local variable or method `object' for #<#<Class:0x000055e83a1e2530>:0x000055e83cbba6f0>
        Did you mean?  object_id
            app/views/shared/_error_messages.html.erb:1:in `_app_views_shared__error_messages_html_erb__812901978115774136_47227970213020'
            app/views/password_resets/edit.html.erb:7:in `block in _app_views_password_resets_edit_html_erb___393354210430426510_47227969853980'
            app/views/password_resets/edit.html.erb:6:in `_app_views_password_resets_edit_html_erb___393354210430426510_47227969853980'
            test/integration/password_resets_test.rb:39:in `block in <class:PasswordResetsTest>'

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 5.616371100011747]
 test_micropost_interface#MicropostsInterfaceTest (5.62s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

  59/59: [=================================] 100% Time: 00:00:08, Time: 00:00:08

Finished in 8.64566s
59 tests, 299 assertions, 1 failures, 1 errors, 0 skips

PasswordResetsTestで以下のエラーが発生するようになりました。

ActionView::Template::Error: undefined local variable or method `object'

また、スタックトレースには以下の記載が残されています。

app/views/password_resets/edit.html.erb:7
app/views/password_resets/edit.html.erb:6

エラーメッセージのパーシャルを書き換えたので、既存のソースコードも書き換える必要がある

エラーメッセージのパーシャル、とりわけ引数の渡し方を変更したため、エラーメッセージのパーシャルを使用している既存実装も書き換える必要が発生しました。具体的には、app/views/password_resets/edit.html.erb内の実装ですね。

app/views/password_resets/edit.html.erb
  <% provide(:title, 'Reset password') %>
  <h1>Reset password</h1>

  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
-       <%= render 'shared/error_messages' %>
+       <%= render 'shared/error_messages', object: f.object %>

        <%= hidden_field_tag :email, @user.email %>

        <%= f.label :password %>
        <%= f.password_field :password, class: 'form-control' %>

        <%= f.label :password_confirmation %>
        <%= f.password_field :password_confirmation, class: 'form-control' %>

        <%= f.submit "Update password", class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>

app/views/password_resets/edit.html.erbの実装を変更した時点でのテストの結果

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2691
Started with run options --seed 19709

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

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

Finished in 4.36191s
1 tests, 5 assertions, 1 failures, 0 errors, 0 skips

test/integration/microposts_interface_test.rbの22行目で、「リダイレクトされるべきところがリダイレクトされていない」というメッセージを出してテストが失敗しています。

私の環境では、test/integration/microposts_interface_test.rbの17〜22行目のコードは以下のようになっています。

test/integration/microposts_interface_test.rb(22行目)
# 有効な送信
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

無効なマイクロポストの送信に対する処理の実装は完成できたと判断できます。今度は、「有効な内容のマイクロポストを送信した場合に対する処理」を実装する必要があります。

有効なマイクロポストのPOSTに対する処理

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

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      #TODO: ここに有効なマイクロポストの投稿に対する処理を書く <- この部分の実装
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end

  # ...略
end

有効なマイクロポストがPOSTされた場合、RDBのMicropostsテーブルへの保存が成功します。すなわち、@micropost.savetrueを返すということになります。

有効なマイクロポストのPOSTに対する処理の内容

Railsチュートリアル本文において、RDBのMicropostsテーブルへの保存が成功した場合の処理は以下のようになる旨が記載されています。

  1. マイクロポストの投稿が成功した旨をフラッシュメッセージで表示する
  2. Homeページ( / )にリダイレクトする

対応するコードは以下です。

flash[:success] = "Micropost created!"
redirect_to root_url

有効なマイクロポストのPOSTに対する処理の実装

では、app/controllers/microposts_controller.rbに対応するコードを追加していきましょう。

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

    def create
      @micropost = current_user.microposts.build(micropost_params)
      if @micropost.save
-       #TODO: ここに有効なマイクロポストの投稿に対する処理を書く
+       flash[:success] = "Micropost created!"
+       redirect_to root_url
      else
        @feed_items = []
        render 'static_pages/home'
      end
    end

    def destroy
    end

    private

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

有効なマイクロポストのPOSTに対する処理を実装した時点でのテストの結果

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 36
Started with run options --seed 50895

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

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

Finished in 5.49914s
1 tests, 8 assertions, 1 failures, 0 errors, 0 skips

私の環境では、test/integration/microposts_interface_test.rbの25〜26行目は以下のコードが記述されています。

test/integration/microposts_interface_test.rb(25〜26行目)
# 投稿を削除する
assert_select 'a', text: 'delete'

「マイクロポストの削除用リンクが描画されていない」という趣旨のメッセージですね。

有効なマイクロポストのPOSTに対する処理の実装までで成功するようになったテストの内容

なお、以下のテストについては、ここまでの実装で問題なく成功しています。

test/integration/microposts_interface_test.rb(17〜24行目)
# 有効な送信
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

「今投稿したばかりのマイクロポストの内容が、リダイレクト後のHomeページに描画されている」というテストも成功しています。

マイクロポストを削除する機能をMicropostsリソースに追加する

前提条件

「ユーザーは、自身が投稿したマイクロポストのみを削除することができる」という動作となることが前提です。

また、Railsチュートリアル本文には、「マイクロポストの削除リンクを含むHomeページのモックアップ」として、図 13.16が示されています。

違うユーザーのマイクロポストを削除しようとした際の動作に対するテストを追加する

違うユーザーのマイクロポストを削除しようとした場合、以下の動作となる必要があります。

  • マイクロポストは削除されず、RDB上のマイクロポストの数も変化しない
  • Homeページ( / )にリダイレクトされる

こちらはtest/controllers/microposts_controller_test.rbに追加していきます。テストの名前は「should redirect destroy for wrong micropost」とします。

test/controllers/microposts_controller_test.rb
  require 'test_helper'

  class MicropostsControllerTest < ActionDispatch::IntegrationTest

    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

+   test "should redirect destroy for wrong micropost" do
+     log_in_as(users(:rhakurei))
+     micropost = microposts(:ants)
+     assert_no_difference 'Micropost.count' do
+       delete micropost_path(micropost)
+     end
+     assert_redirected_to root_url
+   end
  end

マイクロポストのパーシャルに削除リンクを追加する

動作のポイントは以下です。

  • マイクロポストを投稿したユーザー自身のみに削除リンクが表示される
  • 実際の削除の前に、「You sure?」という確認メッセージを出力する

上述動作を踏まえた上で、マイクロポストの削除リンクのコードは以下のようになります。

<% if current_user?(micropost.user) %>
  <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %>
<% end %>

マイクロポストの削除リンクのコードを、app/views/microposts/_micropost.html.erbに追加していきます。

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: { confierm: "You sure?" } %>
+     <% end %>
    </span>
  </li>

マイクロポストのパーシャルに削除リンクを追加した時点でのテストの結果

マイクロポストのパーシャルに削除リンクを追加した時点での、test/integration/microposts_interface_test.rbを対象としたテストの結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 49
Started with run options --seed 16580

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 5.129021706999993]
 test_micropost_interface#MicropostsInterfaceTest (5.13s)
        "Micropost.count" didn't change by -1.
        Expected: 38
          Actual: 39
        test/integration/microposts_interface_test.rb:28:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 5.13155s
1 tests, 9 assertions, 1 failures, 0 errors, 0 skips

私の環境では、test/integration/microposts_interface_test.rbの28〜30行目は以下のコードが記述されています。

test/integration/microposts_interface_test.rb(28〜30行目)
assert_difference 'Micropost.count', -1 do
  delete micropost_path(first_micropost)
end

"Micropost.count" didn't change by -1.というのは、「正しいdeleteリクエストが送られたのに、RDB上から対象マイクロポストが削除されていない」という旨のメッセージですね。今度は、「正しくマイクロポストの削除処理が行われるdestroyアクションの実体」を実装する必要がありますね。

マイクロポストの削除処理の実体を実装する

マイクロポストの削除処理は、対象となるマイクロポストを指定したHTTPのDELETEリクエストをトリガーとして行われます。そのため、マイクロポストの削除処理の実体というのは、具体的には「Micropostsコントローラーのdestroyアクション」ということになります。

マイクロポストの削除処理に必要となる実装

繰り返しになりますが、「ユーザーは、自身が投稿したマイクロポストのみを削除することができる」という動作となることが前提です。そのため、beforeフィルターの内容も、「削除対象のマイクロポストが、現在のユーザーと紐付いたものであるか」を検査するものである必要があります。

当該beforeフィルターの具体的な動作は以下のようになります。

  1. DELETEリクエストのクエリパラメータとして与えられたマイクロポストをキーとして、現在のユーザーの全マイクロポストにfind_byメソッドで検索を行う
  2. 検索結果に該当するマイクロポストがあった場合のみ、実際にマイクロポストの削除処理を行う

検索結果に該当するマイクロポストがなかった場合は、マイクロポストの削除処理を行わずにHomeページ( / )にリダイレクトします。

beforeフィルターの内容

上記実装のうち、beforeフィルターに関する部分のみを取り出すと以下のようになります。beforeフィルターに対応するメソッドの名前は、correct_userとします。

before_action :correct_user, only: [:destroy]

private

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

Micropostsコントローラーのdestroyアクションの内容

一方、実際にRDBからマイクロポストを削除する処理を実装するのは、Micropostsコントローラーのdestroyアクション内となります。その動作は以下の通りです。

  1. RDBから当該マイクロポストを削除する
  2. マイクロポストが削除された旨をフラッシュメッセージで表示する
  3. リダイレクトする

対応するコードは以下のようになります。

Microposts#destroy
def destroy
  @micropost.destroy
  flash[:success] = "Micropost deleted"
  redirect_to request.referrer || root_url
end

request.referrerというメソッドの意味

先程、単に「リダイレクトする」と書きました。実は、このときのリダイレクト先としては、以下の2つのURLを設定しています。

  • DELETEリクエストが発行されたページ
  • DELETEリクエストが発行されたページが特定できなかった場合)Homeページ

request.referrerというのは、上記のコードの場合、「DELETEリクエストが発行されたページ」を指します。

マイクロポストを削除する機能をMicropostsリソースに追加した時点でのテストの結果

マイクロポストを削除する機能をMicropostsリソースに追加した時点での、test/integration/microposts_interface_test.rbを対象としたテストの結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 75
Started with run options --seed 25888

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

Finished in 2.92451s
1 tests, 10 assertions, 0 failures, 0 errors, 0 skips

続いてはtest/controllers/microposts_controller_test.rbを対象としたテストの結果です。

# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 380
Started with run options --seed 1158

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

Finished in 1.18872s
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

今度はテストスイート全体に対してテストを実施してみましょう。

# rails test
Running via Spring preloader in process 107
Started with run options --seed 2017

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

Finished in 10.45135s
59 tests, 316 assertions, 0 failures, 0 errors, 0 skips

テストスイート全体も無事テストが成功しています。

実はこの時点でHomeページの実装が足りない

…あれ、ちょっと待ってください。Homeページの実装が足りないのにテストが全部成功してしまいました。具体的には、以下の実装が足りていません!

  • サイドバーで表示されるべきユーザー情報のパーシャル
  • マイクロポストの投稿フォーム

というわけで、上記2つの要素に対しての実装が必要となります。こちらも、Railsチュートリアル本文の記述を参考としつつ、テスト駆動で実装していきましょう。以下別記事となります。

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