正しいフォーム
前提条件
<form class="new_user" id="new_user" action="/users" accept-charset="UTF-8" method="post" kpxcform-initialized="true" kpxcusername="user_email" kpxcpassword="user_password">
Railsにより実際に構築されたHTMLのうち、form
要素の開始タグのみを取り出したものです。タグの属性から、以下のことがわかります。
- このフォームでsubmitすると、
/users
に対してHTTPリクエストを送出する - このフォームが送出するHTTPリクエストの種類は
POST
である
Railsの仕様上、/users
に対するPOST
リクエストはcreate
アクションによって処理されるようになっています。
このフォームでsubmitしてからの一連の処理
-
create
アクションで、フォームによって生成されたHTTPリクエストを受け取る -
User.new
メソッドによって新しいユーザーオブジェクトが生成される - ユーザーオブジェクトの情報をRDBMSに保存する処理を行う
- 再度ユーザー登録ページを表示する
「ユーザーオブジェクトの情報をRDBMSに保存する」という処理は、必ず成功するとは限らず、失敗することもあります。成功時と失敗時では違う処理を実装する必要があります。
ユーザー登録の失敗に対応できるcreate
アクション
class UsersController < ApplicationController
...略
+
+ def create
+ @user = User.new(params[:user]) # TODO:実装はまだ終わっていない
+ if @user.save
+ # TODO: 保存の成功をここに実装する
+ else
+ render 'new'
+ end
end
ここで重要なポイントは以下です。
-
@user.save
は、保存が成功した場合にtrue
、保存が失敗した場合にfalse
を返す -
render
メソッドは、コントローラ中でも正常に動作する
実際に無効なユーザー登録データを送信(submit)してみる
現時点で実際に無効なユーザー登録データを送信(submit)してみると、Webブラウザには以下のようなエラー画面が出力されます。

rails server
のログには、以下のようなエラーメッセージが残されています。
Started POST "/users" for 172.17.0.1 at 2019-10-19 05:19:50 +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#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"wwiu2NvcdY59JxI/Eq96qLM88Oe8oAsBPMM7RQ1JT4xO47Pt+wSscw8fK40/S+DAa8h5ziATCnlV5gxUWDsiag==", "user"=>{"name"=>"", "email"=>"", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"}
Completed 500 Internal Server Error in 199ms (ActiveRecord: 41.4ms)
ActiveModel::ForbiddenAttributesError (ActiveModel::ForbiddenAttributesError):
app/controllers/users_controller.rb:12:in `create'
HTTPリクエスト中のuser
パラメータハッシュと、それに対してRailsが行う処理
rails server
のログから、HTTPリクエストの内容のうちのuser
パラメータハッシュだけを抜粋してみます(内容は見やすいように整形します)。
"user"=>{ "name"=>"",
"email"=>"",
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]"
}
ここで重要なポイントは以下です。
- HTTPリクエストの内容は、そのものが
params
というハッシュとしてUsersコントローラに渡される - ユーザー登録情報の送信においては、
params
ハッシュの中にさらにハッシュが含まれる(hash-of-hashes)
ここで改めて、ユーザー登録フォームのinput
要素の一つを見てみましょう。
<input type="email" name="user[email]" id="user_email" data-kpxc-id="user_email" kpxc-username-field="true">
今回着目するのはname
属性です。実際の動作は以下のようになります。
- HTTPリクエストが組み立てられ、フォームがsubmitされる
- Railsは、受け取ったHTTPリクエストの内容からハッシュを構成する
- 上述ログにおけるハッシュのキーが
"user"
などの文字列である -
params
ハッシュの属性名は、HTTPリクエストに存在するクエリパラメータの名前を元に構成される - クエリパラメータの名前は、フォームの各構成要素の
name
属性を元に構成される
- 上述ログにおけるハッシュのキーが
- コントローラは、
params
ハッシュ内の:user
ハッシュを引数としてUser.new
メソッドを実行する- ここまで「
param
ハッシュ」と言ってきたが、param
の型は、実はRailsで定義されたActionController::Parameters
型である -
ActionController::Parameters
型(およびそのスーパークラスであるActiveSupport::HashWithIndifferentAccess
型)は、キーが文字列("user"
)であるかシンボル(:user
)であるかを区別しないように作られている - 結果として、上述のハッシュのキーの数および名前は、
User.new
の引数で必要となるデータと完全に一致する
- ここまで「
長くなりました。特に重要そうなポイントは以下だと思います。
- HTMLフォーム内各要素の
name
属性が、Railsでどう処理されるか -
param
ハッシュについて -
ActionController::Parameters
型とその特徴 - (復習)
User.new
の引数で必要となるデータ
マスアサインメント
実は、以下2つのコードは、Railsにおいてほぼ同じ挙動をします。
params[:user] = { name: "Foo Bar",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar"
}
@user = User.new(params[:user])
@user = User.new(name: "Foo Bar", email: "foo@invalid",
password: "foo", password_confirmation: "bar")
バージョン3.xまでのRailsには、「1つのモデルに対し、@user = User.new(params[:user])
のようにして当該モデルのフィールドをまとめて設定できる」という機能がありました。この機能をマスアサインメント機能といいます。
しかしながら、バージョン4.x以降のRailsでは、@user = User.new(params[:user])
というようなコードを実行するとエラーになります。マスアサインメント機能には深刻な脆弱性が存在し、その対策が実装されたためです。次の項目「Strong Parameters」で解説していきます。
Strong Parameters
マスアサインメント機能に起因する脆弱性
しかしながら、このようなマスアサインメント系のメソッドは、無条件に全てのデータを上書きしてしまうため、「マスアサインメント機能に起因する脆弱性」というものが生まれることになってしまいました。
どのような脆弱性か
例えば、以下のような実装がなされたRailsアプリケーションがあるとしましょう。
- Userモデルに
admin
という属性がある -
admin
属性がtrue
であるユーザーは、Webサイトの管理者権限を持つ
このような実装でマスアサインメントが可能な場合、params[:user]
にadmin='1'
なるクエリパラメータを紛れ込ませて渡してしまえば、admin
属性をtrue
にできてしまいます。こうした属性を含むparams
ハッシュが丸ごとUser.new
に渡されてしまうと何が起こるでしょうか。「どのユーザーでもadmin='1'
をHTTPリクエストに紛れ込ませることでWebサイトの管理者権限を奪うことができる」という事態が発生してしまうのです。
当該脆弱性への対策
Rails 4.0では、コントローラ層でStrong Parametersという機能を使うことにより、マスアサインメント脆弱性への対策としています。Strong Parameters機能で実現できることは以下のとおりです。
- 必須のパラメータと許可されたパラメータを指定する
-
params
ハッシュを直接渡すとエラーが発生するようにする
現在のアプリケーションにおける、Strong Parameters機能の仕様
現在のアプリケーションでは、以下の仕様にしたいと考えています。
-
params
ハッシュにおいて、:user
属性を必須とする -
params
ハッシュの:user
属性では、以下の属性を許可し、それ以外の属性は許可しない- 名前(
user[:name]
) - メールアドレス(
user[:email]
) - パスワード(
user[:password]
) - パスワードの確認(
user[:password_confirmation]
)
- 名前(
上述仕様に対応するメソッドチェーンは以下となります。
params.require(:user).permit(:name, :email, :password, :password_confirm)
このメソッドチェーンの戻り値は、許可された属性のみが含まれたparams
のハッシュです(:user
属性がない場合はエラー)。
上記動作をまとめると以下の通りです。
-
params.require
の引数にシンボルを与えることにより、params
ハッシュに当該シンボルをキーとするハッシュが含まれていることが必須となる -
params.require(:symbol).permit
の引数にシンボルを与えることにより、params[:symbol]
ハッシュに以下の条件が定義される-
params.require(:symbol).permit
の引数に与えられたシンボルをキーとするハッシュ値が含まれることを許可する -
params.require(:symbol).permit
の引数に与えられたシンボル以外をキーとするをハッシュ値が含まれることを許可しない
-
user_params
メソッドと、Rubyのprivate
キーワード
params.require(:user).permit(:name, :email, :password, :password_confirm)
これはいかにも長いメソッド呼び出しです。そのままでは使いにくいです。こうした長いメソッド呼び出しをより使いやすくするため、Railsではuser_params
1という外部メソッドを使うのが慣習になっています。
user_params
は、Strong Parameters機能が有効な環境で、params[:user]
の代わりに使われる外部メソッドです。Strong Parameters機能で適切に初期化されたハッシュを返します。
Rubyのprivate
キーワード
今回実装するuser_params
メソッドは、もっぱらUsersコントローラの内部でのみ使われ、Web経由で外部ユーザにさらされる必要はありません。そのため、Railsのprivate
キーワードによって外部から使えないように制限します。
create
アクションで実際にStrong Parameters機能を活用する
class UsersController < ApplicationController
...略
def create
- @user = User.new(params[:user]) # TODO:実装はまだ終わっていない
+ @user = User.new(user_params)
if @user.save
# TODO: 保存の成功をここに実装する
else
render 'new'
end
end
+
+ private
+
+ def user_params
+ params.require(:user).permit(:name, :email, :password, password_confirmation)
+ end
end
なお、Railsチュートリアル本文では、「private
キーワード以降のコードを強調するために、user_params
のインデントを1段深くしていること」について、ここでより突っ込んだ解説をしています。以下の理由でよろしいのだとか。
- クラス内に多数のインデントがある場合、privateメソッドの場所を簡単に見つけられるようになる
- インデントがない場合に比べ、どこからprivateになるのかについて困惑しなくなる
ここまでの実装が完了すれば、「送信ボタンを押してもエラーが出ないユーザー登録フォーム」が実現します。しかしながら、現在のところ、(開発者用のデバッグ領域を除いて)間違った情報を送信しても何もフィードバックは返ってきません。現状で間違った情報を送信した後のWebブラウザの表示内容は以下のようになります。

デバッグ領域の内容を見れば「間違った情報が送られた」ということはわかるのですが、それ以外は新規にユーザー登録画面を開いた場合と何ら変わりありません。これでは利用者が困惑することは避けられません。
さらに、有効なユーザー情報を送信しても新しいユーザーが実際に作成されることはありません。
以上の問題は、今後の実装にて解決していきます。
演習 - Strong Parameters
1. /signup?admin=1 にアクセスし、params
の中にadmin
属性が含まれていることをデバッグ情報から確認してみましょう。
http://localhost:8080/signup?admin=1
にアクセスした場合、デバッグ領域の内容は以下のようになります。

確かにadmin: '1'
が含まれていますね。
なお、このときのrails server
のログは以下のようになります。
Started GET "/signup?admin=1" for 172.17.0.1 at 2019-10-20 12:55:02 +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#new as HTML
Parameters: {"admin"=>"1"}
...略
Completed 200 OK in 951ms (Views: 900.6ms | ActiveRecord: 0.0ms)
同様に、例えば /signup?foobar=1 にアクセスすると、params
の中にはfoobar
属性が含まれます。
エラーメッセージ
Railsによるエラーメッセージの自動生成
Railsは、モデルオブジェクトの検証でエラーが発生した場合、発生したエラーに対応するエラーメッセージを自動で生成する機能を有しています。
# rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?> password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]
以上は、「無効なメールアドレス、かつ、短すぎるパスワード」というUserモデルのオブジェクトを保存しようとした場合の経過です。user.save
が失敗した時点で、@user
オブジェクトに関連付けられたエラーメッセージの配列がerrors.full_messages
にされます。
Railsによって自動生成されたエラーメッセージをRailsアプリケーションで使う方法
@user
オブジェクトに関連付けられたエラーメッセージをRailsアプリケーションで使う、例えばエラーメッセージをWebブラウザに表示することももちろん可能です。今回は、「ユーザーのnew
ページでエラーメッセージのパーシャル(partial)を出力する」という方法をとります。
<% provide(:title, 'Sign up') %>
<h1>Sign up</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 %>
+ <%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
- <%= f.email_field :email %>
+ <%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
- <%= f.password_field :password %>
+ <%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
- <%= f.password_field :password_confirmation %>
+ <%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
class: 'form-control'
というコードは、「Bootstrapがうまく扱えるようにクラスを追加する」という意味のコードです。
上述コードにおける主要な変更内容は、「shared/error_messages
というパーシャルを描画(render)する機能を実装した」という点です。より細かく見ていくと、以下のような点が重要です。
- 複数のビューで使われるパーシャルは、専用のディレクトリである
shared
に置かれる - 現時点で
app/views/shared
というディレクトリは存在しないので、新規にディレクトリを作成する必要がある -
app/views/shared/_error_messages.html.erb
というファイルを新規に作成する必要がある-
error_messages
は部分テンプレートなので、ファイル名の頭には_
が必要である
-
フォーム送信時にエラーメッセージを表示するためのパーシャルと、その解説
早速必要なディレクトリとファイルを生成してみましょう。
>>> pwd
~/docker/rails_tutorial_test/sample_app
>>> mkdir app/views/shared
>>> touch app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(@user.errors.count, "error") %>.
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
app/views/shared/_error_messages.html.erb
には、色々と興味深い新要素が登場します。
errors.count
メソッド
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?> password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.count
=> 2
errors.count
メソッドは、エラーの数を返すメソッドです。errors
に限らず、配列一般に対し、Ruby標準のメソッドとしてcount
メソッドが定義されています。
any?
メソッド
>> user.errors.any?
=> true
>> user.errors.empty?
=> false
Enumerableモジュールが実装されたオブジェクト(配列やハッシュ等)に対して定義される、Ruby標準のメソッドです。
- レシーバ2に一つでも戻り値が
true
となる値が格納されていればtrue
- レシーバが空である場合、戻り値は
false
- レシーバに値が格納されていても、その全ての戻り値が
false
となる場合、戻り値はfalse
Railsチュートリアル本文には、「empty?
メソッドとany?
メソッドは互いに補完する」「any?
メソッドはちょうどempty?
と逆の動作で、要素が1つでもある場合はtrue
、ない場合はfalse
を返す」とありますが、実際には「empty?
もany?
も戻り値がfalse
になる場合」というのが存在します。例えば以下のような場合です。
>> [false, false, nil, nil].any?
=> false
>> [false, false, nil, nil].empty?
=> false
「empty?
もany?
も戻り値がfalse
になるのは、全ての要素がfalse
またはnil
のいずれかである場合」ですね。
Railsのエラーオブジェクトに限れば、その性質上「全ての要素がfalse
またはnil
のいずれかになる」という事態は発生しないはずなので、「any?
メソッドは、要素が1つでもある場合はtrue
、ない場合はfalse
を返す」という認識で構わないでしょう。
helper.pluralize
メソッド
こちらはRailsのActiveSupportライブラリで定義されているメソッドです。第1引数に整数を、第2引数に文字列を取ります。第1の引数に基づき、第2引数(英単語であることが前提です)が単数形になるか複数形になるかを判断した上で、必要なら複数形に変換し、文字列を出力します。
rails console
から動作を試してみることもできます。
>> helper.pluralize(1, "error")
=> "1 error"
>> helper.pluralize(3, "error")
=> "3 errors"
今回のコードでは、pluralize
が以下のように使われています。
<%= pluralize(@user.errors.count, "error") %>
その結果は、@user.errors.count
の内容に応じ、例えば"0 errors"
,"1 error"
,"2 errors"
のようになります。結果として、"1 errors"
のような英文法に合わないメッセージが出現する事態を自動的に避けることが可能になります。
文字列に対するpluralize
メソッド
RailsのActiveSupportライブラリでは、helper.pluralize
のみならず、一般の文字列に対するpluralize
メソッドも定義されています。レシーバーとして与えられた文字列を(英単語であることを前提として)複数形に変換します。
>> "error".pluralize
=> "errors"
>> "query".pluralize
=> "queries"
不規則活用の名詞を複数形にすることもできます。
>> "woman".pluralize
=> "women"
>> "erattum".pluralize
=> "eratta"
>> "person".pluralize
=> "people"
文字列に対するsingularize
メソッド
RailsのActiveSupportライブラリでは、singularize
メソッドも定義されています。
なお、helper.singularize
は定義されていないようです。pluralize
メソッドとは逆に、レシーバーとして与えられた文字列を(英単語であることを前提として)単数形に変換します。
>> "errors".singularize
=> "error"
>> "women".singularize
=> "woman"
>> "people".singularize
=> "person"
pluralize
やsingularize
に関する注意事項
pluralize
やsingularize
といったメソッドは、ActiveSupport::Inflector
というライブラリにて定義されています。
こうした便利なメソッドの恩恵を享受するためには、変数やメソッドなどの名前は英語で定義されている必要があります。Railsに限らず、純粋なRubyも英語を強く意識した言語仕様になっています。特にRails開発においては、pluralize
やsingularize
によって単数形と複数形を変換して使うのが前提になっているので、変数やメソッドの名前を英語以外の言語で定義するのは避ける必要があります。
エラーメッセージにスタイルを与えるためのCSS
使用可能なCSSセレクタ
改めてapp/views/shared/_error_messages.html.erb
には、error_explanation
というCSS IDを持つdiv
要素が含まれています。
<div id="error_explanation">
...略
</div>
またRailsは、「無効な内容の送信によって元のページに戻される」という動作が発生すると、エラーに関するHTMLの内容のみを、クラスfield_with_errors
が定義されたdiv
要素で自動的に囲うという動作をします。
結果、エラーメッセージにスタイルを与えるためには、以下2つのCSSセレクタを使用できることになります。
-
error_explanation
というCSS ID -
field_with_errors
というCSSクラス
実際にSCSSを書いてみる
@import "bootstrap-sprockets";
@import "bootstrap";
...略
/* forms */
...略
+
+ #error_explanation {
+ color: red;
+ ul {
+ color: red;
+ margin: 0 0 30px 0;
+ }
+ }
+
+ .field_with_errors {
+ @extend .has-error;
+ .form_control {
+ color: $state-danger-text;
+ }
+ }
...略
このコードのポイントは「Sassの@extend
関数により、Bootstrapのhas-error
というCSSクラスが適用されている」という点です。$state-danger-text
というのもBootstrap由来ですね。
ここまで実装してきたものを踏まえ、改めてエラーメッセージをWebブラウザで表示させてみる
ここまでに、以下の実装を行ってきました。
- Userモデルの
errors
オブジェクトを元に、エラーの内容を得られるようにした - ユーザー登録が失敗した場合に、以下の情報が表示されたユーザー登録画面が出力されるようにした
- エラーが発生した事実
- 入力内容の何が悪くてエラーが発生したのか
- SCSSにより、ユーザー登録画面に表示されるエラーメッセージの見栄えを整えた
以上の実装を踏まえた上で、再びユーザー登録を失敗させてみましょう。

エラーメッセージがきちんと表示されていますね。全てのエラーをfixすれば、RDBに問題なく保存できる形のデータになりそうです。エラーメッセージの見栄えも整っています。
同じエラーメッセージが2回表示されてしまう問題
しかしながら、上記スクリーンショットにおいては、「Password can't be blank」というエラーメッセージが2回表示されています。原因は、以下2つのバリデーションで、両方とも空のパスワード(nil
)を検知してしまうためです。
-
presence: true
によるバリデーション -
has_secure_password
によるバリデーション
この問題を解決するには、今後のRailsチュートリアルで登場するallow_nil: true
というオプションを追加すればよいのだそうです。
演習 - エラーメッセージ
1. 最小文字数を5に変更すると、エラーメッセージも自動的に更新されることを確かめてみましょう。
「最小文字数を変更する」というのは、app/models/user.rb
を変更するのでしたね。以下の変更を加えます。
class User < ApplicationRecord
...略
- validates :password, presence: true, length: { minimum: 6 }
+ validates :password, presence: true, length: { minimum: 5 }
end
結果は以下のように変化します。確かに「minimum is 5 characters」というエラーメッセージが見受けられますね。

2. 未送信のユーザー登録フォーム (図 7.12) のURLと、送信済みのユーザー登録フォーム (図 7.18) のURLを比べてみましょう。なぜURLは違っているのでしょうか? 考えてみてください。
前提条件は以下です。
- 図 7.12では、URLが
/signup
となっている - 図 7.18では、URLが
/users
となっている
簡潔に言えば理由は以下です。
- 図 7.12は、
/signup
にGET
リクエストを送出した結果である - 図 7.18は、
/users
にPOST
リクエストを送出した結果である
/users
にPOST
リクエストを送出した結果が「ユーザー登録に失敗」であった場合、Usersコントローラにより「new
ビューがrender
される」という処理が行われます。結果として、エラーメッセージが表示されている以外は/signup
にGET
リクエストを送出した結果と同じものが表示されることになるのです。
失敗時のテスト
- ブラウザでフォームを表示する
- 有効なデータと無効なデータを交互に流し込む
- どちらの場合にもアプリケーションが正常に動作することを確認する
- 多くの場合、Excel様式にスクリーンショットを貼り付けていく
- アプリケーションに変更が生じるたびに、まったく同じテストを繰り返す
このような「人海戦術でカバー」式のやり方は、はっきり言って担当者にとって苦痛なものです。しかしながら、Railsをはじめとする現代のWebフレームワークでは、こうしたフォームのテストをも自動化することが可能なのです。
新規ユーザー登録用の統合テストを作成する
開発環境でrails generate integration_test users_signup
というコマンドを実行します。
- 統合テストなので
integration_test
- 新規ユーザー登録用のテストなので
users_signup
- 「リソース名は複数形」というRailsの慣習に従った名前である
# pwd
/var/www/sample_app
# rails generate integration_test users_signup
Running via Spring preloader in process 204
invoke test_unit
create test/integration/users_signup_test.rb
今実装するのは「失敗時のテスト」ですが、今後実装する「成功時のテスト」にも、今回生成したファイルを使っていきます。
今回実装するテストで確認するもの
今回実装するテストは、ユーザー登録ボタンを押したときに(ユーザー情報が無効であるため)ユーザーが作成されないことを確認するものです。また、今後うっかり要素を変更してしまっても気付けるようにするために、ユーザー登録フォーム内に存在するHTML要素についてもテストしていきます。
User.count
メソッドを使用する
ユーザーが作成されないことを確認するための方法としては、「User.count
メソッドを用いて、RDB上に存在するユーザーの数をカウントする」という方法を取ります。
>> User.count
(2.3ms) SELECT COUNT(*) FROM "users"
=> 1
assert_select
メソッドを使用する
HTML要素をテストするためには、「assert_select
メソッドを使用する」という方法を取ります。
テストの内容について解説
get
メソッドを使ってユーザー登録ページにアクセスする
ユーザー登録ページにアクセスするために、まずsignup_path
に対してget
メソッドを実行します。/signup
にGET
リクエストを実行するのと同じ操作ですね。
get signup_path
ルーティングが正しく書かれていれば、Usersコントローラのnew
メソッドが実行され、最終的にユーザー登録ページが返ってくるはずです。
users_path
にPOST
リクエストを送信する
新規ユーザー登録に際してユーザー登録フォームをsubmitしたときの動作は、「users_path
にPOST
リクエストを送信する」というものでした。この動作をテストするには、post
メソッドを含む以下のようなコードで実現できます。
assert_no_difference 'User.count' do
post users_path, params: { user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" } }
end
ブロック内のコードで重要なポイントは以下です。
-
post
の引数- 第1引数には、
POST
リクエストを送信する先のパスが与えられる - 第2引数には、
POST
リクエストの内容が与えられる
- 第1引数には、
-
post
の第2引数に、params[:user]
ハッシュを与えている- Rails 4.2以前では、
params
を暗黙的に省略しても正常に動作した-
user
ハッシュのみでも正しく動作する、ということ
-
- Rails 5.0以降は、
params
を省略することは非推奨とされている-
params
ハッシュを明示的に含めることが推奨される
-
- Rails 4.2以前では、
-
params[:user]
において、ユーザー登録情報として正しくない内容になるようなハッシュを与えている-
:name
の値が空である -
:email
の値が、メールアドレスとして正しくない文字列である -
:password
、:password_confirm
共に短すぎる -
:password
と:password_confirm
が一致していない
-
assert_no_difference 'User.count'
については次の項で解説します。
assert_no_difference 'User.count'
とは
assert_no_difference 'User.count' do
# ...略
end
assert_no_difference
というのは、「ブロックの実行前後で引数(今回はUser.count
)の値が変化しないことをテストする」というメソッドです。結果、このテスト全体としては、以下のコードと等価になります。
before_count = User.count
post user_path, ...
after_count = User.count
assert_equal before_count, after_count
しかしながら、assert_no_difference
のほうが明確な記法ですし、Rubyの慣習にも従った記法となります。
ユーザー登録は、技術的にはユーザー登録フォームを経由しなくても可能である
assert_no_difference
ブロック内では、get
メソッドを使用していません。これは以下のことを意味します。
-
get
、post
、assert
等の各メソッドに技術的な関連性がない - ユーザー登録フォームを経由せず、直接
post
メソッドを呼び出してユーザー登録を行うことが可能である
ただ、(実際の手順にならって)get
とpost
の両メソッドをを呼び出すことには、以下の理由から十分な価値があります。
- テストのコンセプトを明確にするため
- ユーザー登録ページそのものを改めてチェックするため
実際にテストを実装する
test/integration/users_signup_test.rb
の初期状態
生成されたばかりのtest/integration/users_signup_test.rb
は、以下の内容になっています。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end
実際にテストを実装したときのtest/integration/users_signup_test.rb
の内容
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, params: { user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" } }
end
assert_template 'users/new'
end
end
Railsチュートリアル本文にある以下の記述には注意が必要です。
送信に失敗したときにnewアクションが再描画されるはずなので、assert_templateを使ったテストも含めている
ここまでの学習で、このテストが通るために必要な機能は全て実装してきました。そのため、このテストは問題なく通ります。
# rails test integration
Running via Spring preloader in process 250
Started with run options --seed 50920
20/20: [=================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.51197s
20 tests, 41 assertions, 0 failures, 0 errors, 0 skips
演習 - 失敗時のテスト
1. リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。
リスト 7.25にテンプレートを用意しておいたので、参考にしてください。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
...略
assert_template 'users/new'
+ assert_select 'div#error_explanation'
+ assert_select 'div.alert' do
+ assert_select 'div.alert-danger'
+ end
+ assert_select 'ul'
+ assert_select 'li'
end
end
私は以下のテストを追加してみました。
-
error_explanation
というIDを持つdiv
要素が存在する -
alert
とalert-danger
両方のクラスを持つdiv
要素が存在する -
ul
要素が存在する -
li
要素が存在する
ul
とli
というのは、「実際にユーザーデータの不適合の内容が1つ以上表示されているか」をテストするためのアサーションです。
なお、「alert
とalert-danger
両方のクラスを持つdiv
要素が存在することをテストする」というのは、初見では方法がわからないかと思います。Rails上におけるテストで、「1つのHTML要素に対し、複数のクラスが定義されていること」をテストする正しい書き方 - Qiitaにて、その点について解説を入れてみました。
2.1. ユーザー登録フォームのURLは /signup ですが、無効なユーザー登録データを送付するとURLが /users に変わってしまいます。リスト 7.26とリスト 7.27の内容を参考に、この問題を解決してみてください。
これはリスト 5.43で追加した名前付きルート (/signup) と、RESTfulなルーティング (リスト 7.3) のデフォルト設定との差異によって生じた結果です。
そういえば、演習 - エラーメッセージの設問2にもこの点についての言及が出てきましたね。
まず、config/routes.rb
を編集し、/signup に対するPOST
リクエストでUsersコントローラのcreate
アクションが実行されるようにします。
Rails.application.routes.draw do
get 'users/new'
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
+ post '/signup', to: 'users#create'
resources :users
end
続いて、 /signup に対応するビューであるapp/views/users/new.html.erb
を編集し、フォームがPOST
リクエストを送出する先が /signup になるようにします。
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
- <%= form_for(@user) do |f| %>
+ <%= form_for(@user, url: signup_path) do |f| %>
...略
<% end %>
</div>
</div>
2.2. (2.1. の続きです)うまくいけばどちらのURLも /signup になるはずです。あれ、でもテストはgreen
のままになっていますね...、なぜでしょうか? (考えてみてください)
# rails test integration
Running via Spring preloader in process 363
Started with run options --seed 26654
20/20: [=================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.13368s
20 tests, 46 assertions, 0 failures, 0 errors, 0 skips
この時点においても、テストは確かに通ります。
私としては、この場合にテストが問題なく通るのは「少なくともRESTfulなアーキテクチャを前提とした場合、『GET
とPOST
(あるいはPATCH
やDELETE
でも)で、URLが同じだが、Webアプリケーションで実際に呼び出される処理が異なる』というのは、仕組み上正しい挙動であるから」というのが理由と考えています。
3. リスト 7.25のpost
部分を変更して、先ほどの演習課題で作った新しいURL (/signup) に合わせてみましょう。また、テストが greenのままになっている点も確認してください。
変更するファイルはtest/integration/users_signup_test.rb
です。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
- post users_path, params: { user: { name: "",
+ post signup_path, params: { user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" } }
end
...略
end
end
改めてテストを実行してみましょう。
p# rails test integration
Running via Spring preloader in process 400
Started with run options --seed 64185
20/20: [=================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.33716s
20 tests, 46 assertions, 0 failures, 0 errors, 0 skips
テストは問題なく通りますね。
4. リスト 7.27のフォームを以前の状態 (リスト 7.20) に戻してみて、テストがやはりgreen
になっていることを確認してください。
これは問題です! なぜなら、現在postが送信されているURLは正しくないのですから。
app/views/users/new.html.erb
を以下のように変更した場合にテストは通るか、という話です。
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
- <%= form_for(@user, url: signup_path) do |f| %>
+ <%= form_for(@user) do |f| %>
...略
<% end %>
</div>
</div>
現状でテストを実行してみましょう。
# rails test integration
Running via Spring preloader in process 452
Started with run options --seed 53403
20/20: [=================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.06687s
20 tests, 46 assertions, 0 failures, 0 errors, 0 skips
テストが通ってしまいました。
現状でテストが通ることの何が問題か
何が問題かといいますと…
<form class="new_user" id="new_user" action="/users" accept-charset="UTF-8" method="post">
...略
</form>
上記form
要素の属性において、action
属性の値が/users
になっていることが問題なのです。action
属性の値は、本来は/signup
でなければなりません。現状ではテストが通ってしまうことが逆に問題となります。
4.2. assert_select
を使ったテストをリスト 7.25に追加し、このバグを検知できるようにしてみましょう (テストを追加してred
になれば成功です)。
ヒント: フォームから送信してテストするのではなく、
'form[action="/signup"]'
という部分が存在するかどうかに着目してテストしてみましょう。
test/integration/users_signup_test.rb
を以下のように変更します。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
+ assert_select 'form[action="/signup"]'
...略
end
end
ユーザー登録フォームのHTMLの内容が問題なので、/signup に対してget
メソッドを実行した直後がテストを行う適切なタイミングになりますね。そのため、post
メソッドを実行する前に当該テストを実行するようにしています。
改めてテストを実行してみましょう。
# rails test integration
Running via Spring preloader in process 478
Started with run options --seed 25723
FAIL["test_invalid_signup_information", UsersSignupTest, 2.1218616999976803]
test_invalid_signup_information#UsersSignupTest (2.12s)
Expected at least 1 element matching "form[action="/signup"]", found 0..
Expected 0 to be >= 1.
test/integration/users_signup_test.rb:7:in `block in <class:UsersSignupTest>'
20/20: [=================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.12470s
20 tests, 40 assertions, 1 failures, 0 errors, 0 skips
めでたく(?)テストが通らなくなりました。Expected at least 1 element matching "form[action="/signup"]", found 0.
という内容の指摘事項なので、実装したいテストがきちんと実装されていることになりますね。
4.3. その後、変更後のフォーム (リスト 7.27) に戻してみて、テストがgreen
になることを確認してみましょう。
app/views/users/new.html.erb
を以下のように変更します。
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
- <%= form_for(@user) do |f| %>
+ <%= form_for(@user, url: signup_path) do |f| %>
...略
<% end %>
</div>
</div>
改めてテストを実行してみましょう。
# rails test integration
Running via Spring preloader in process 491
Started with run options --seed 19848
20/20: [=================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.41956s
20 tests, 47 assertions, 0 failures, 0 errors, 0 skips
無事にテストが通りました。