そもそもこの章で何をするか
この章で新たに実装する要素は以下です。
- Usersリソースの以下のアクション
edit
update
index
destroy
「ユーザーを更新する」というセクションでは、User#edit
の機能を実装し、「ログインユーザーが自身の登録情報を編集する」ことを可能にします。
ユーザー情報更新
実装全体の流れは、新規ユーザー登録の流れと類似しています。
-
edit
ビューを実装する -
PATCH
リクエストに応答するupdate
アクションを実装する
実装内容 | 新規ユーザー登録時 | ユーザー情報更新時 |
---|---|---|
ビューを出力するアクション | new |
edit |
使用するHTTPリクエスト | POST |
PATCH |
HTTPリクエストに応答するアクション | create |
update |
大きな相違点は、「新規ユーザー登録は誰でも可能だが、ユーザー情報更新は当該ユーザーのみが可能である」ということです。「当該ユーザーのみが可能」ということは、当然アクセス制御が必要になります。その実装には、RDBの更新のみならず、RDBの読み込みや認証等の機能も絡んできます。
Railsにおいては、認証機能が実装されているならば、beforeフィルターによってアクセス制御が可能です。
編集フォーム
ユーザー情報の編集には、まず編集フォームが必要となります。Railsチュートリアル本文においては、編集フォームのモックアップとして、図 10.1が示されています。
edit
アクションの定義
edit
アクションを定義せずにビューだけ作ってしまうと…
以下のスクリーンショット・ログのようなエラーが出てしまいます。
後述するビューの定義において、@user
には有効なUserモデルの実体が必要とされます。しかしながら、edit
アクションがなければ、@user
に有効なUserモデルの実体は与えられません。したがって、以下のようなエラーになります。
Started GET "/users/2/edit" ...略
Processing by UsersController#edit as HTML
Parameters: {"id"=>"2"}
Rendering users/edit.html.erb within layouts/application
Rendered users/edit.html.erb within layouts/application (16.4ms)
Completed 500 Internal Server Error in 68ms (ActiveRecord: 0.0ms)
ActionView::Template::Error (First argument in form cannot contain nil or be empty):
3:
4: <div class="row">
5: <div class="col-md-6 col-md-offset-3">
6: <%= form_for(@user) do |f| %>
7: <%= render 'shared/error_messages' %>
8:
9: <%= f.label :name %>
app/views/users/edit.html.erb:6:in `_app_views_users_edit_html_erb__939350102988132727_69984448042620'
実際にedit
アクションを定義する
Userリソースの /edit (例えば /users/1/edit)に対するGET
リクエストに対して@user
の内容を定義するためには、app/controllers/users_controller.rb
でedit
アクションに定義を記述していきます。HTTPリクエストから対象ユーザーのIDを取り出すには、params[:id]
変数を使うのでしたね。
class UsersController < ApplicationController
...略
+ def edit
+ @user = User.find(params[:id])
+ end
private ...略
end
HTMLを生成するための埋め込みRubyのソース
Usersリソースのedit
に対応するビューは、touch app/views/users/edit.html.erb
というファイルに記述していきます。現時点で当該ファイルは存在しないため、まず当該ファイルを生成する必要があります。ファイルの生成はtouch
コマンドですね。
>>> touch app/views/users/edit.html.erb
app/views/users/edit.html.erb
の内容は、以下の通りとなります。
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form_control' %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form_control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form_control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form_control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
ユーザー新規作成フォームと同様、以下のようなフィールドをもつ入力フォームを定義しています。
- 名前の入力フィールド
- メールアドレスの入力フィールド
- パスワードの入力フィールド
- パスワードの確認の入力フィールド
- submitボタン
また、この他に、Grabatarへのリンクも挿入されています。
編集ページの描画
ここまで実装した時点でユーザー編集ページをWebブラウザで表示すると、その描画結果は以下のようになります。
編集画面における名前やメールアドレスは、Railsによって既存のUserリソースから読み出された値が自動で入力されます。
Railsの内部処理について - フォーム内容が酷似する新規ユーザー作成時と既存ユーザー編集時の比較から
PATCH
リクエストをどう発行する?
RESTアーキテクチャにおいて、既存リソースの内容を更新する際には、POST
メソッドではなくPATCH
メソッドが発行されることが要求されます。
一方で、2019年11月現在、HTMLフォームが発行できるHTTPメソッドは、GET
またはPOST
のみです。RESTで要求されるPUT
、PATCH
、DELETE
といったHTTPメソッドは、HTMLフォームのみでは発行できません。
HTMLフォームのみでは発行できないHTTPメソッドを発行したい場合、Webフレームワーク側で対応することになります。Railsにおいては、「POST
リクエストと隠しinput
フィールドにより、PATCH
リクエストを"偽造"する」という内部実装が行われています。
<input name="_method" type="hidden" value="patch" />
新規ユーザー用のPOST
リクエストと既存ユーザー用のPATCH
リクエスト。Railsは内部でどう区別する?
少なくともRailsチュートリアルにおいて、新規ユーザー用のPOST
リクエストと既存ユーザー用のPATCH
リクエストの内容は全く同じになります。とすると、Railsはこの2つのリクエストをどう区別するのでしょうか。
このようなリクエストが送られてきた場合、Railsは内部でActiveRecordのnew_record?
メソッドを呼び出します。その結果がtrue
であればPOST
、false
であればPATCH
と判断する、という次第です。
#rails console --sandbox
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false
ナビゲーションバーの内容を更新する
ユーザー編集ページを実装したので、ナビゲーションバーの内容も更新する必要があります。
<%= link_to "Settings", edit_user_path(current_user) %>
この埋め込みRubyでは、以下の技術を用いています。
- 名前付きルート
edit_user_path
-
current_user
ヘルパー
実装箇所はapp/views/layouts/_header.html.erb
です。以前との差分を以下に記述します。
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
...略
<nav>
<ul class="nav navbar-nav navbar-right">
...略
<% if logged_in? %>
...略
<li class="dropdown">
...略
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
- <li><%= link_to "Settings", '#') %></li>
+ <li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: :delete %>
</li>
</ul>
</li>
<% else %>
...略
<% end %>
</ul>
</nav>
</div>
</header>
演習 - 編集フォーム
1. Gravatarの編集ページへのリンクに、"noopener
"という値を定義したrel
属性を追加しましょう。
target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング (Phising) サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。
この問題点を衝いた攻撃手法は、Tabnabbingと呼ばれます。タブナビング(Tabnabbing)とは | セキュリティ用語解説 | 日立ソリューションズの情報セキュリティブログ
Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。
対処方法は、リンク用のaタグのrel (relationship) 属性に、"noopener"と設定するだけです。
具体的には、以下のような変更になりますすね。
- <a href="http://gravatar.com/emails" target="_blank">change</a>
+ <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
app/views/layouts/_header.html.erb
全体の変更内容は以下のようになります。
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
...略
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
- <a href="http://gravatar.com/emails" target="_blank" >change</a>
+ <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
</div>
</div>
</div>
2. リスト 10.5のパーシャルを使って、new.html.erbビュー (リスト 10.6) とedit.html.erbビュー (リスト 10.7) をリファクタリングしてみましょう (コードの重複を取り除いてみましょう)。
まず、空のapp/views/layouts/_form.html.erb
を作成するところから始まります。
>>> pwd
~/docker/rails_tutorial_test/sample_app
>>> touch app/views/layouts/_form.html.erb
作成したapp/views/users/_form.html.erb
のソースを、次のように編集していきます。
<%= form_for(@user, url: yield(:path)) do |f| %>
<%= render 'shared/error_messages', object: @user %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>
app/views/users/_form.html.erb
の内容を埋め込むビューのソースコードに対しても編集を加えていきます。以下2つのファイルが編集対象となります。
app/views/users/new.html.erb
app/views/users/edit.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:button_text, 'Create my account') %>
<% provide(:path, signup_path) %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
</div>
</div>
<% provide(:title, 'Edit user') %>
<% provide(:button_text, 'Save changes') %>
<% provide(:path, user_path) %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
</div>
</div>
</div>
追記
この部分の実装に手こずりました。Railsチュートリアル - Webフォームのコードを共通化する際に、フォームのPOST先をどうyieldするか - Qiitaにて、手こずった内容のみを切り出して記述しています。
編集の失敗
update
アクションの実装方針
以下の方針をとっていきます。
-
@user.update_attributes
の戻り値に応じ、成功と失敗を判断し、分岐する - 失敗した場合の処理から実装していく
- 失敗した場合の処理の実装が完了した後に、成功した場合の処理を実装する
この実装方針、create
アクションの実装と似ていますね。
update
アクションの最初の実装
実装内容としては以下の通りです。
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
# TODO:更新に成功した場合の実装
else
render 'edit'
end
end
app/controllers/users_controller.rb
への追加は以下のようになります。
class UsersController < ApplicationController
...略
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
def edit
@user = User.find(params[:id])
end
+ def update
+ @user = User.find(params[:id])
+ if @user.update_attributes(user_params)
+ # TODO:更新に成功した場合の実装
+ else
+ render 'edit'
+ end
+ end
private ...略
現状では、以下のような動作になります。
- 更新に成功した場合は何もしない
- 更新に失敗した場合、編集ページをレンダリングする
ソースコードを見ても、やはりcreate
アクションの実装と似ていますね。
現時点でも、無効な情報を送信すると、何が無効であるかのエラーメッセージがレンダリングされる
create
アクションの実装時点で、Userモデルのバリデーションとエラーメッセージのパーシャルを実装していました。そのため、update
アクションにおいても、無効な情報を送信すると何が無効であるかのエラーメッセージがレンダリングされます。例えば以下のスクリーンショットは、実際にエラーメッセージがレンダリングされている状況のものです。
演習 - 編集の失敗
1. 編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。
前提
例えば、ID=2のユーザーに、以下のような情報を送信した場合を考えてみます。
- Name…Hoge Hoge
- Email…foobar@invalid
- Password…(何も入力しない)
- Password Confirmation…(何も入力しない)
上記の情報は、以下の理由で有効ではありません。
- メールアドレスの形式がルールに合わない
- パスワードを空にはできない
- パスワードが短すぎる
rails server
の挙動
「Save changes」ボタンをクリックし、その後のrails server
の挙動を追いかけてみます。
Started PATCH "/users/2" ...略
PATCH
リクエストが無事に発行されたようです。
Processing by UsersController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"+kYFeAEo8aAbvma48y+scR6qlRq4T+V+7UICoVuzt3BjQl1eaeMq4y0x0XBoDp2KYxsodGIn0Y6qAxcXW6Y0zw==", "user"=>{"name"=>"Hoge Hoge", "email"=>"foobar@invalid", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Save changes", "id"=>"2"}
リクエストのパラメーターの内容も問題ないようです。Railsの内部的な処理でいえば、params
ハッシュに入ってくる値ですね。
User Load (2.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
(0.2ms) begin transaction
User Exists (7.1ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ? [["email", "foobar@invalid"], ["id", 2], ["LIMIT", 1]]
(0.2ms) rollback transaction
rollback transaction
というメッセージが出ています。(ユーザー情報が無効なため)変更内容のRDBへの反映がなされずにトランザクションが終了した、ということですね。
Rendering users/edit.html.erb within layouts/application
Rendered shared/_error_messages.html.erb (3.4ms)
Rendered users/_form.html.erb (31.4ms)
Rendered users/edit.html.erb within layouts/application (52.7ms)
Rendered layouts/_rails_default.erb (292.4ms)
Rendered layouts/_shim.html.erb (0.4ms)
User Load (4.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Rendered layouts/_header.html.erb (8.8ms)
Rendered layouts/_footer.html.erb (1.4ms)
Completed 200 OK in 580ms (Views: 500.5ms | ActiveRecord: 15.1ms)
改めてedit
ビューがレンダリングされ、HTTPリクエストは 200 OK というコードが返ってきて終了しています。_error_messages.html.erb
というパーシャル、すなわち、エラーメッセージもレンダリングされています。
編集失敗時のテスト
統合テストの作成
edit
アクションに対する統合テストは、以下のコマンドで作成します。
# rails generate integration_test users_edit
Running via Spring preloader in process 425
invoke test_unit
create test/integration/users_edit_test.rb
統合テストなのでintegration_test
、Userリソースのedit
アクションに対するテストなのでusers_edit
という名前、ということですね。
編集の失敗に対する簡単なテスト
test/integration/users_edit_test.rb
には、はじめにUsersEditTest
というクラスが生成されます。
class UsersEditTest < ActionDispatch::IntegrationTest
end
既存のユーザー登録情報をfixtureから取得する
「既存のユーザー登録情報の内容を変更する」という動作なので、まずは「既存のユーザー登録情報」そのものが必要となります。そのため、まずはじめにsetup
メソッドを定義し、既存のユーザー登録情報をfixtureから取得するようにします。
def setup
@user = users(:rhakurei)
end
「編集失敗時のテスト」そのものを定義する
「編集失敗時のテスト」そのものを、「unsuccessful edit」という名前で定義します。
test "unsuccessful edit" do
end
edit
ビューが描画されるかどうかを確認する
get edit_user_path(@user)
assert_template 'users/edit'
編集ページにGET
メソッドでアクセスした上で、editビューが描画されるかどうかを確認しています。ここで渡すページのパスは、user_path
ではなくedit_user_path
である必要があります。
正しくないユーザー情報でPATCH
リクエストを発行する
patch user_path(@user), params: { user: { name: "",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar" } }
@user
を引数に取ったuser_path
1に対し、params
ハッシュをパラメータとしてPATCH
リクエストを発行しています。
params
ハッシュは、user
ハッシュを値として取っています。user
ハッシュは、あらゆる意味で正しくないユーザー情報を示すハッシュです。何が正しくないかを以下に示します。
-
name
キーに対する値が空文字列である -
email
キーに対する文字列が、メールアドレスとして正しくない形式である -
password
キーに対する値とpassword_confirmation
キーに対する値が一致しない -
password
キーに対する値もpassword_confirmation
キーに対する値も、いずれも短すぎる(6文字を下回る)
正しくないのは以上4点です。
正しくないユーザー情報でPATCH
リクエストを発行したことにより、Editビューが描画されることを確認する
assert_template 'users/edit'
リダイレクトなしでEditビューが描画されることを確認します。
全体のソースコード
現時点でテストが成功することを確認する
現時点では、「たった今実装した、Editページに対するテスト」「テストスイート全体」いずれに対してもテストは成功するはずです。早速やってみましょう。
# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 459
Started with run options --seed 46024
1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.91981s
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
# rails test
Running via Spring preloader in process 446
Started with run options --seed 39430
30/30: [=================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.24308s
30 tests, 77 assertions, 0 failures, 0 errors, 0 skips
無事テストが成功しましたね。
演習 - 編集失敗時のテスト
1. リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみてましょう。
ヒント: 表 5.2で紹介した
assert_select
を使ってalert
クラスのdiv
タグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。
assert_select
の使い方
ハイパーリンク以外にassert_select
を適用する場合、第1引数は以下のような形の文字列で与えることができます。
- 存在を期待する要素名(例…
assert_select "div"
) - 存在を期待する要素名#ID名(例…
assert_select "div#profile"
) - 存在を期待する要素名.クラス名(例…
assert_select "div.nav"
)
いずれの場合も、第2引数は要素の期待する内容となります(例…assert_select "div.nav", "foobar"
)。
今回のテスト内容に対応するソースコード
結果、今回のテスト内容に対応するソースコードは以下の通りになります。
assert_select "div.alert", "The form contains 4 errors."
test/integration/users_edit_test.rb
のソースコード全体の変更内容は以下の通りになります。
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:rhakurei)
end
test "unsuccessful edit" do
...略
assert_template 'users/edit'
+ assert_select "div.alert", "The form contains 4 errors."
end
end
テストを実行してみる
test/integration/users_edit_test.rb
に対してテストを実行してみます。
# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 472
Started with run options --seed 15929
1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.89699s
1 tests, 3 assertions, 0 failures, 0 errors, 0 skips
テストは無事成功しました。
テストを失敗させてみる
このテストは本当に正しい実装なのでしょうか。
先ほど、「今回与えるユーザー情報で、正しくない点は4点である」と言及しました。なので、「The form contains 4 errors.」となるわけですね。例えば、「4 errors」を「3 errors」にすれば、テストは失敗するはずです。
- assert_select "div.alert", "The form contains 4 errors."
+ assert_select "div.alert", "The form contains 3 errors."
上述の変更を保存した上で、再度テストを実行してみます。
# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 485
Started with run options --seed 61445
FAIL["test_unsuccessful_edit", UsersEditTest, 1.6028200000000652]
test_unsuccessful_edit#UsersEditTest (1.60s)
<The form contains 3 errors.> expected but was
<The form contains 4 errors.>..
Expected 0 to be >= 1.
test/integration/users_edit_test.rb:16:in `block in <class:UsersEditTest>'
1/1: [===================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.60985s
1 tests, 3 assertions, 1 failures, 0 errors, 0 skips
無事(?)想定した形でテストが失敗しました。
TDDで編集を成功させる
そもそも「ユーザー情報の編集の成功」とはどういう状態であるか
PATCH
リクエストのパラメータとして、有効なユーザー情報が与えられた場合を前提とします。このとき、以下の動作すべてが実現されることをもって「ユーザー情報の編集の成功」とします。
- フラッシュメッセージが空でない
- 編集された当該ユーザーのプロフィールページにリダイレクトされる
- RDB内のユーザー情報が正しく更新される
- パスワード・パスワードの確認がいずれも空である場合、パスワードを変更せずに他の属性のみを編集できる
ユーザー情報の編集の成功に対する振る舞いを実装するためのテスト
「編集成功時のテスト」そのものを定義する
「編集失敗時のテスト」そのものを、「successful edit」という名前で定義します。
test "successful edit" do
end
edit
ビューが描画されるかどうかを確認する
get edit_user_path(@user)
assert_template 'users/edit'
この部分は編集失敗時と同じになります。
有効なユーザー名とメールアドレス
有効なユーザー名とメールアドレスを変数に格納します。
name = "Foo Bar"
email = "foo@bar.com"
後で「PATCH
リクエストで与えたユーザー情報の内容とRDBに反映された内容は本当に同じか?」ということを確認する必要があるため、この時点で変数として定義しています。
正しいユーザー情報でPATCH
リクエストを発行する
patch user_path(@user), params: {user: { name: name,
email: email,
password: "",
password_confirm: ""} }
@user
を引数に取ったuser_path
1に対し、params
ハッシュをパラメータとしてPATCH
リクエストを発行しています。
先ほど変数に格納したユーザー名とメールアドレスの形式は正しいものであるため、現時点でも正しいユーザー情報となります。但し、「パスワード・パスワードの確認がいずれも空である場合、パスワードを変更せずに他の属性のみを編集する」動作については別途定義する必要があります。
フラッシュメッセージが空でないことを確認する
assert_not flash.empty?
フラッシュメッセージが空でなければテストが通ります。これは、「ユーザーの更新が正常に完了したならば、更新が完了した旨メッセージを出力する」という実装が確実になされるようにするためです。
PATCH
リクエストがリダイレクトを返し、リダイレクト先が更新が行われたユーザーのプロフィールページであることを確認する
assert_redirected_to @user
PATCH
リクエストがリダイレクトを返し、そのリダイレクト先が「更新が行われたユーザーのプロフィールページ(へのGET
リクエスト)であればテストが通ります。実際にリダイレクト先に移動するわけではないため、follow_redirect!
メソッドは実行しません。
ユーザーのプロフィールページを再読み込みし、変更内容がRDBに正しく反映されたことを確認する
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
まず、PATCH
リクエストが返すリダイレクト先に移動しないでユーザーのプロフィールページを更新します。その上で、以下の事柄を確認します。
- RDBに保存された当該ユーザー情報の
name
カラムの値が、PATCH
リクエストのパラメータとして与えたname
の属性値と一致すること - RDBに保存された当該ユーザー情報の
email
カラムの値が、PATCH
リクエストのパラメータとして与えたemail
の属性値と一致すること
いずれも、一致すればテストが通ります。
テスト"successful edit"
の全体像
ここまでの内容を踏まえて、テスト"successful edit"
をtest/integration/users_edit_test.rb
に実装していきます。
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:rhakurei)
end
...略
+ test "successful edit" do
+ get edit_user_path(@user)
+ assert_template 'users/edit'
+ name = "Foo Bar"
+ email = "foo@bar.com"
+ patch user_path(@user), params: {user: { name: name,
+ email: email,
+ password: "",
+ password_confirm: ""} }
+ assert_not flash.empty?
+ assert_redirected_to @user
+ @user.reload
+ assert_equal name, @user.name
+ assert_equal email, @user.email
+ end
end
現時点でテスト"successful edit"
は成功しない
当然ながら、現時点でテスト"successful edit"
は成功しません。
# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 537
Started with run options --seed 34712
FAIL["test_successful_edit", UsersEditTest, 2.7275522999989334]
test_successful_edit#UsersEditTest (2.73s)
Expected true to be nil or false
test/integration/users_edit_test.rb:28:in `block in <class:UsersEditTest>'
2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.79524s
2 tests, 5 assertions, 1 failures, 0 errors, 0 skips
最初に表面化する失敗は、「空であってはいけないフラッシュメッセージが空である」というものです。
test/integration/users_edit_test.rb:28
とありますが、私の環境では以下の行でした。
assert_not flash.empty?
Usersコントローラーに、編集成功時の動作を実装する
app/controllers/users_controller.rb
における、編集成功時の実装コードそのものは、新規ユーザー作成成功時の実装コードとほぼ同じです。
class UsersController < ApplicationController
...略
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
+ flash[:success] = "Profile updated"
+ redirect_to @user
else
render 'edit'
end
end
private ...略
end
実際の相違点は、flash[:success]
の後のメッセージのみですね。
UsersController#edit
の実装が完了しても、まだテスト"successful edit"
は成功しない
UsersController#edit
の実装は完了しましたが、この時点ではまだテスト"successful edit"
は成功しません。
# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 550
Started with run options --seed 1354
FAIL["test_successful_edit", UsersEditTest, 1.954527699999744]
test_successful_edit#UsersEditTest (1.96s)
Expected true to be nil or false
test/integration/users_edit_test.rb:28:in `block in <class:UsersEditTest>'
2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.01404s
2 tests, 5 assertions, 1 failures, 0 errors, 0 skips
現段階でテストが通らない原因
テストが通らない原因は、テスト内容と実装の以下の差異が原因です。
- Userモデルにおいては、「パスワードの長さは最短6文字以上」というバリデーションが実装されている
- 一方、テスト内容においては、パスワード・パスワードの確認ともに空白文字列が与えられている
結果、現在のテスト内容では、Userモデルにおけるパスワードのバリデーションに抵触し、RDBの内容が変更されるには至りません。
Userモデルの定義を書き換え、パスワードが空のままでもRDBを更新できるようにする
「パスワード・パスワードの確認がいずれも空である場合、パスワードを変更せずに他の属性のみを編集する」という機能を実装し、テスト"successful edit"
を成功させるためには、Userモデルにおけるパスワードのバリデーションの内容を変更する必要があります。具体的には、Userモデルのvalidates
にallow_nil: true
というオプションを追加します。
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
class User < ApplicationRecord
...略
has_secure_password
- validates :password, presence: true, length: { minimum: 6 }
+ validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
# 渡された文字列のハッシュ値を返す
def self.digest(unencrypted_password)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
BCrypt::Password.create(unencrypted_password, cost: cost)
end
...略
end
「パスワードが空のままでも更新できるようにする」とあります。しかしながら、空のパスワードそのものが有効なユーザー情報に反映されることはありません。なぜなら、パスワードのバリデーションについては、validates
によるもののほか、has_secure_password
によるものが別途存在するためです。
但し、has_secure_password
によるパスワードのバリデーションに抵触しても、パスワード以外の属性に対する変更はRDBに反映されます。こうして、「パスワード・パスワードの確認がいずれも空である場合、パスワードを変更せずに他の属性のみを編集する」という動作が実現するわけなのです。
今度こそテスト"successful edit"
が成功する
ここまで実装を終えたところで、改めてusers_edit_test.rb
に記述されたテストを実行してみましょう。
# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 578
Started with run options --seed 8165
2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.42118s
2 tests, 9 assertions, 0 failures, 0 errors, 0 skips
今度こそテストが成功しました!
演習 - TDDで編集を成功させる
1. 実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。
パスワード変更を伴わない場合
以下のような内容をユーザー編集フォームに入力します。ポイントは以下です。
- 名前・メールアドレスともに有効である
- メールアドレスはGrabatarに登録されているものとする
- 名前は変更する
- メールアドレスは変更しない
- パスワード・パスワード確認はいずれも空欄とする
「Save changes」ボタンをクリックしてみます。
無事編集が成功しました。
rails server
に出力されたPATCH
リクエストのログは、以下のようになっています。
Started PATCH "/users/2" for 172.17.0.1 at 2019-11-17 11:31:28 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"j1DoIERut6dteOJnSRI/cjIx+rvGT5aMNdc0ZcbxLMYWVLAGLKVs5Fv3Va/SMw6JT4BH1RwnonxyliHTxuSveQ==", "user"=>{"name"=>"Foo bar baz", "email"=>"[Gravatarに登録されているメールアドレス]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Save changes", "id"=>"2"}
User Load (5.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
(0.2ms) begin transaction
User Exists (4.9ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ? [["email", "[Gravatarに登録されているメールアドレス]"], ["id", 2], ["LIMIT", 1]]
SQL (13.1ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Foo bar baz"], ["updated_at", "2019-11-17 11:31:28.978381"], ["id", 2]]
(13.1ms) commit transaction
Redirected to http://localhost:8080/users/2
Completed 302 Found in 55ms (ActiveRecord: 36.5ms)
SQLのUPDATE
文を見ると、name
属性値のみ更新するUPDATE
文が発行されていますね。
2. もしGravatarと紐付いていない適当なメールアドレス (foobar@example.comなど) に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみてましょう。
以下のような内容をユーザー編集フォームに入力します。foobar.foobar@example.com というメールアドレスは、Gravatarと紐付いていません。
「Save changes」ボタンをクリックしてみます。
プロフィール画像は、Gravatarのデフォルト画像になりました。
-
どのアクションに関連付けられるかは、
config/routes.rb
内のresources :users
という記述により定義されています。 ↩