はじめに
最近、プロジェクト管理業務が業務の大半を占めており、
プログラムを書く機会がなかなかありません。
このままだとプログラムがまったく書けない人になってしまう危機感(迫り来る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のみ必要となる。
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
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
- micropost
- POSTリクエストを送信する
- ログイン画面にリダイレクトされること
- Micropostモデルの数をカウントし、数が変化していないこと
-
should redirect destroy when not logged in
- Micropostモデルの数をカウントし、数が変化していないこと
- DELETEリクエストを送信する
- micropost_path(@micropost)
- DELETEリクエストを送信する
- ログイン画面にリダイレクトされること
- Micropostモデルの数をカウントし、数が変化していないこと
上記の仕様で実装する。
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コントローラからも使用する。
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!のメソッドが残ってる・・・。ここまで長い道のりでした。
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フィルタを実装する。
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属性を許可する。
private
def micropost_params
params.require(:micropost).permit(:content)
end
ログインしているユーザーのマイクロポストを生成し、DBに保存する。
成功すればroot_urlへリダイレクトする。
失敗すればstatic_pages配下のhomeページを呼び出す。
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を促す
<% 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ヘルパーで英語の複数形に変換
<%= 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ボタン
<%= 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アクションにマイクロポストのインスタンス変数を定義する。
ログインしているユーザのマイクロポストを取得する。
class StaticPagesController < ApplicationController
def home
@micropost = current_user.microposts.build if logged_in?
end
【view】エラーメッセージパーシャルのリファクタリング
form_forで定義する変数が、画面によって変化する。
- ユーザー登録画面:@user
- マイクロポスト投稿画面:@micropost
そのため、エラーメッセージパーシャルの呼び出し方法は、
複数のインスタンス変数に対応するためにobject: f.object
となる。
<% form_for(@micropost) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
そのため、エラーメッセージパーシャル内で
もともと@user
固定で呼び出していた箇所を、object変数を使うように
リファクタリングする。
■修正前
<% 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 %>
■修正後
<% 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
にリファクタリングする。
<%= form_for(@user, url: yield(:url)) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
2.パスワード再登録画面
エラーメッセージパーシャルの呼び出し部分に、object: f.object
を追加する。
・・・略・・・
<%= 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
ブラウザからアクセスできることを確認。
演習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
ログイン時に表示するパーシャル。
<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>
未ログイン時に表示するパーシャル。
<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ページ。
かなりスッキリしました。
<% 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が入る際にエスケープされる)
# 試作feedの定義
# 完全な実装は次章の「ユーザーをフォローする」を参照
def feed
Micropost.where("user_id = ?", id)
end
【controller】static_pagesコントローラのリファクタリング
homeアクションのリファクタリングを行う。
1行だった後置if文を、前置if文に変更する。
また、前項で実装したfeedメソッドの取得結果をインスタンス変数@feed_itemsに格納する。
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
<% if @feed_items.any? %>
<ol class="microposts">
<%= render @feed_items %>
</ol>
<%= will_paginate @feed_items %>
<% end %>
【view】homeページヘの組み込み
13.3.2の演習で作成したパーシャルから、フィード表示パーシャルの呼び出しを行う。
<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>
動作確認
追加したフィードが表示されていることを確認。
ただし、マイクロポストの投稿に失敗した場合は、エラーが発生する。
(@feed_itemsを期待しているが取得できていない)
対策として、DB保存失敗時には空の配列で@feed_itemsを定義する。
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保存失敗時にエラーが発生しないことを確認。
演習1
新しく実装したマイクロポストの投稿フォームを使って、実際にマイクロポストを投稿してみましょう。Railsサーバーのログ内にあるINSERT文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。
投稿フォームからマイクロポストを投稿する。
ログは以下の通り。
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】マイクロポストのパーシャルのリファクタリング
削除リンクを追加する。
自分が投稿したマイクロポストのみ削除することができる。
<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へリダイレクトする。
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文の内容を確認してみてください。
ブラウザからアクセスし、任意のマイクロポストが削除できることを確認。
ログは以下の通り。
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メソッドを使うようにリファクタリング。
def destroy
@micropost.destroy
flash[:success] = "Micropost deleted"
# redirect_to request.referrer || root_url
redirect_back(fallback_location: root_url)
end
問題なく動くことを確認。
新しいAPI使うほうがいいですね!
13.3.5 フィード画面のマイクロポストをテストする
本章での学び
作成したマイクロポストの単体テストと統合テストを作成する。
【事前準備】テストデータの用意
マイクロポストのfixtureにデータを追加する。
・・・略・・・
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にリダイレクトすることを確認
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属性は空
- params
- microposts_pathへpostリクエストを送信
- HTMLにdiv id=error_explanation要素があることを確認
- 変数contentに文字列を代入
- マイクロポストの数が変わることを確認
- microposts_pathへpostリクエストを送信
- params
- micropost
- content属性は代入した変数content
- params
- microposts_pathへpostリクエストを送信
- root_urlへリダイレクトされること
- リダイレクトを追う
- 代入したcontentの文字列が、HTML内に一致すること
- HTML内にdeleteテキストとaタグがあることを確認
- ユーザの1番目のマイクロポストを取得する
- マイクロポストの数が-1されることを確認
- micropost_pathへdeleteリクエストを送信
- archerユーザのプロフィールページヘgetリクエストを送信
- HTML内にdeleteテキストとaタグが0件であることを確認
上記を踏まえて実装する。
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メソッドをコメントアウトする。
# 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保存に成功した際の処理をコメントアウトする。
@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の描画をコメントアウトする。
<% 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>'
「違うユーザーのプロフィールにアクセス」のテスト
ログインユーザーとマイクロポストのユーザーが一致するかの判定をコメントアウト。
<% #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"と単数形で表示されることを確認
上記を踏まえて実装する。
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処理と似ていたので、理解しやすかったです。