別のユーザーに所属するマイクロポストをfixtureに追加する
今後のテストで使うために、マイクロポストのfixtureに対して、最初のテストで使ったユーザーとは別のユーザーに属するマイクロポストを追加していきます。
...略
+
+ 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
にコードを記述していきます。
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行目には以下のコードが記述されています。
assert_select 'div.pagination'
まずはHomeページにマイクロポストのフィードを表示する部分を実装する必要がありそうですね。
Homeページにマイクロポストのフィードを表示する部分を実装する
Railsチュートリアル本文とは順番が前後しますが、テストの失敗内容を考えると、先に実装するのは「Homeページにマイクロポストのフィードを表示する部分」となります。
Railsチュートリアル本文には、「マイクロポスト投稿フォームと、マイクロポストのフィード表示部分が実装されたHomeページのモックアップ」が、図 13.13として示されています。
現在ログインしているユーザーのマイクロポストを全て取得する
ログイン済みユーザーのHomeページに表示するマイクロポストのフィードの内容は、「当該ユーザーが投稿したマイクロポスト」となります。このことは全てのユーザーに共通します。
「フィードの内容を取得する」という処理の内容は、全てのユーザーに共通する内容です。そのため、当該処理はUserモデルに実装するのが自然な流れとなります。編集対象のファイルはapp/models/user.rb
ですね。
また、当該メソッドの名前はfeed
とします。
feed
メソッドの内容
feed
メソッドの内容は、現在のところは、「where
メソッドにより、ログインユーザー自身の全てのマイクロポストを取得する」というものにしておきます。
# 試作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
に対しては以下の変更を反映していきます。
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
の内容は、さしあたって以下のように変更します。
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
の内容は以下のようになります。
<% 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
を再掲しておきます。
<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
ですね。
<% 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行目の内容は以下のようになっています。
# 無効な送信
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
です。
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_items
がnil
になってしまうこと」がエラーの原因のようですね。
マイクロポストの投稿が失敗した場合に、@feed_items
に空の配列を渡すようにする
Railsチュートリアル本文には、上記動作について、以下のように記述されています。
ただしささいなことではありますが、マイクロポストの投稿が失敗すると、 Homeページは
@feed_items
インスタンス変数を期待しているため、現状では壊れてしまいます。最も簡単な解決方法は、リスト 13.50のように空の配列を渡しておくことです。
実際にそのように実装してみましょう。
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/home
にerror_explanation
クラスを持つdiv
要素がレンダリングされるようにする」必要があります。
マイクロポスト投稿フォームの仮パーシャルを追加する
Railsチュートリアル本文中の記載に従えば、ログイン済みユーザーのマイクロポスト表示画面において「error_explanation
クラスを持つdiv
要素」がレンダリングされるのは、マイクロポスト投稿フォーム内となります。まずは、マイクロポスト投稿フォームとなる予定の仮パーシャルを作成し、そこに「error_explanation
クラスを持つdiv
要素」がレンダリングされるようにしましょう。
<%= 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
の内容も、当該パーシャルを使用するように変更する必要があります。変更内容は以下の通りです。
<% 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 %>
エラーメッセージのパーシャルを再定義する
新たに必要となる実装
<%= 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
の変更内容は以下の通りになります。
- <% 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
内の実装ですね。
<% 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行目のコードは以下のようになっています。
# 有効な送信
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
に対する処理
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.save
がtrue
を返すということになります。
有効なマイクロポストのPOST
に対する処理の内容
Railsチュートリアル本文において、RDBのMicropostsテーブルへの保存が成功した場合の処理は以下のようになる旨が記載されています。
- マイクロポストの投稿が成功した旨をフラッシュメッセージで表示する
- Homeページ( / )にリダイレクトする
対応するコードは以下です。
flash[:success] = "Micropost created!"
redirect_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: ここに有効なマイクロポストの投稿に対する処理を書く
+ 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行目は以下のコードが記述されています。
# 投稿を削除する
assert_select 'a', text: 'delete'
「マイクロポストの削除用リンクが描画されていない」という趣旨のメッセージですね。
有効なマイクロポストのPOST
に対する処理の実装までで成功するようになったテストの内容
なお、以下のテストについては、ここまでの実装で問題なく成功しています。
# 有効な送信
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」とします。
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
に追加していきます。
<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行目は以下のコードが記述されています。
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フィルターの具体的な動作は以下のようになります。
-
DELETE
リクエストのクエリパラメータとして与えられたマイクロポストをキーとして、現在のユーザーの全マイクロポストにfind_by
メソッドで検索を行う - 検索結果に該当するマイクロポストがあった場合のみ、実際にマイクロポストの削除処理を行う
検索結果に該当するマイクロポストがなかった場合は、マイクロポストの削除処理を行わずに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
アクション内となります。その動作は以下の通りです。
- RDBから当該マイクロポストを削除する
- マイクロポストが削除された旨をフラッシュメッセージで表示する
- リダイレクトする
対応するコードは以下のようになります。
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チュートリアル本文の記述を参考としつつ、テスト駆動で実装していきましょう。以下別記事となります。