メールアドレスが有効な場合の処理に対するテストの実装
Railsチュートリアル本文の通りに実装していくとすれば、以下のテストが「メールアドレスが有効な場合の処理に対するテスト」に該当することになります。
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:rhakurei)
end
test "password resets" do
get new_password_reset_path
assert_template 'password_resets/new'
# メールアドレスが無効
post password_resets_path, params: { password_reset: { email: "" } }
assert_not flash.empty?
assert_template 'password_resets/new'
get new_password_reset_path
assert flash.empty?
+ # メールアドレスが有効
+ post password_resets_path, params: { password_reset: { email: @user.email} }
+ assert_not_equal @user.reset_digest, @user.reload.reset_digest
+ assert_equal 1, Actionmailer::Base.deriveries.size
+ assert_not flash.empty?
+ assert_redirected_to root_url
end
end
メールアドレスが有効な場合の処理に対するテストを実装した時点で、テストの結果はどうなるか
「メールアドレスが無効な場合の処理」が完了した時点における、app/controllers/password_resets_controller.rb
のソースコードは以下です。
class PasswordResetsController < ApplicationController
def new
end
def create
if false #TODO: 有効なユーザー情報を与えられるようにする
#TODO: 有効なメールアドレスが与えられた場合の処理を実装する
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
end
テストの結果は以下のようになります。
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 129
Started with run options --seed 13207
FAIL["test_password_resets", PasswordResetsTest, 2.7478716999994504]
test_password_resets#PasswordResetsTest (2.75s)
Expected nil to not be equal to nil.
test/integration/password_resets_test.rb:19:in `block in <class:PasswordResetsTest>'
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.74939s
1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
以下のテストが、「Expected nil to not be equal to nil」というメッセージを返して失敗しています。
assert_not_equal @user.reset_digest, @user.reload.reset_digest
debugger
で確認したところ、@user.reset_digest
と@user.reload.reset_digest
のいずれもnil
となっていました。「現在のところreset_digest
を実装していない」からでしょうか。
ひとまずこの時点で確実なのは、「Userモデルにreset_digest
属性に対する正しい実装が必要である」ということです。
User#create_reset_digest
メソッドの実装
「Userモデルのreset_digest
属性に対する正しい実装」は、パスワード再設定全体の中では、以下の手順に関連しています。
- パスワード再設定用のトークンとダイジェストの組を生成する
- 生成されたパスワード再設定用ダイジェストをRDBに保存する
Railsチュートリアル本文においては、これら一連の処理について、create_reset_digest
メソッドとして定義されています。
早速、当該メソッドと関連する実装を追加していきましょう。
Userモデルにcreate_reset_digest
メソッドと、関連する実装を追加する
class User < ApplicationRecord
- attr_accessor :remember_token, :activation_token
+ attr_accessor :remember_token, :activation_token, :reset_token
...略
+
+ # パスワード再設定の属性を設定する
+ def create_reset_digest
+ self.reset_token = User.new_token
+ update_attribute(:reset_digest, User.digest(reset_token))
+ update_attribute(:reset_sent_at, Time.zone.now)
+ end
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email.downcase!
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
Userモデルに新たに追加した実装は以下です。
-
create_reset_digest
メソッド- パスワード再設定用トークンの生成
- 生成されたトークンに対するダイジェストのRDBへの保存
- パスワード再設定用トークンの生成日時のRDBへの保存
- 仮想属性
:reset_token
に対するゲッターとセッターの追加
PasswordResetsコントローラーで、create_reset_digest
メソッドを使うようにする
class PasswordResetsController < ApplicationController
def new
end
def create
+ @user = User.find_by(email: params[:password_reset][:email].downcase)
- if false #TODO: 有効なユーザー情報を与えられるようにする
+ if @user
- #TODO: 有効なメールアドレスが与えられた場合の処理を実装する
+ @user.create_reset_digest
+ # TODO:メール送信処理を実装する
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
end
PasswordResetsコントローラーに新たに追加した実装は以下です。
-
@user
を使わない仮実装を、@user
を使う正式な実装に変更する -
@user.create_reset_digest
により、パスワード再設定用トークン・ダイジェストの生成が正しく行われるようにする
create_reset_digest
メソッド、および、関連する実装を追加した時点でのテストの結果
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 165
Started with run options --seed 26520
ERROR["test_password_resets", PasswordResetsTest, 3.3415248000001156]
test_password_resets#PasswordResetsTest (3.34s)
NameError: NameError: uninitialized constant PasswordResetsTest::Actionmailer
test/integration/password_resets_test.rb:20:in `block in <class:PasswordResetsTest>'
1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.34828s
1 tests, 4 assertions, 0 failures, 1 errors, 0 skips
今度は以下のテストがエラーを返して失敗するようになりました。
assert_equal 1, Actionmailer::Base.deriveries.size
メールの送信処理が実装されていないことが原因のようです。
なお、Railsチュートリアル本文中の演習 - create
アクションでパスワード再設定を行う場合、この時点で一旦実装を中断した上で演習を行っていきます。
メールの送信処理の実装
「アプリケーションは、パスワード再設定用メールを作成し、フォームで指定されたメールアドレスに送信する」という処理の実装部分です。
Usersモデルにsend_password_reset_email
メソッドを追加する
class User < ApplicationRecord
...略
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
# パスワード再設定の属性を設定する
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
+
+ # パスワード再設定のメールを送信する
+ def send_password_reset_email
+ UserMailer.password_reset(self).deliver_now
+ end
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email.downcase!
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
パスワード再設定用のメールを送信するメソッドは、send_password_reset_email
という名前で定義しています。
…よくよく見ると、send_activation_email
とsend_password_reset_email
のコードって似てますよね。後々リファクタリングの対象として出てくるかもしれません。
PasswordResetsコントローラーで、send_password_reset_email
メソッドを使うようにする
class PasswordResetsController < ApplicationController
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
+ @user.send_password_reset_email
+ #TODO: フラッシュメッセージの定義とルートへのリダイレクト
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
end
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 191
Started with run options --seed 20579
ERROR["test_password_resets", PasswordResetsTest, 2.324982799999816]
test_password_resets#PasswordResetsTest (2.33s)
ArgumentError: ArgumentError: wrong number of arguments (given 1, expected 0)
app/mailers/user_mailer.rb:8:in `password_reset'
app/models/user.rb:61:in `send_password_reset_email'
app/controllers/password_resets_controller.rb:9:in `create'
test/integration/password_resets_test.rb:18:in `block in <class:PasswordResetsTest>'
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.32883s
1 tests, 3 assertions, 0 failures, 1 errors, 0 skips
app/mailers/user_mailer.rb
の8行目で、「引数の数が0でなければならないのに、実際には1つの引数が渡されている」というエラーでテストが失敗しています。
def password_reset
今度はパスワード再設定用のメイラーメソッドを定義する必要がありそうですね。
パスワード再設定用のメイラーメソッドpassword_reset
において、引数の定義を変更する
まずは、現在のテストの失敗原因として指摘されている「引数の数が足りない」という問題を解決します。
class UserMailer < ApplicationMailer
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
- def password_reset
+ def password_reset(user)
- @greeting = "Hi"
-
- mail to: "to@example.org"
end
end
password_reset
の引数の定義を変更した時点でのテストの結果
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 232
Started with run options --seed 65406
ERROR["test_password_resets", PasswordResetsTest, 2.472823599999174]
test_password_resets#PasswordResetsTest (2.47s)
NameError: NameError: uninitialized constant PasswordResetsTest::Actionmailer
test/integration/password_resets_test.rb:20:in `block in <class:PasswordResetsTest>'
1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.47449s
1 tests, 4 assertions, 0 failures, 1 errors, 0 skips
(byebug) Actionmailer::Base.deriveries.size
*** NameError Exception: uninitialized constant PasswordResetsTest::Actionmailer
nil
debugger
で調べたところ、「uninitialized constant PasswordResetsTest::Actionmailer」というのは、「Actionmailer::Base.deriveries.size
が初期化されていないこと」が原因で発生しているようです。「password_reset
メソッドの実効的な定義と、テキストメール・HTMLメールそれぞれのテンプレートが必要になる」ということでしょうか。
password_reset
メソッドの実効的な定義と、テキストメール・HTMLメールそれぞれのテンプレート
password_reset
メソッドの実効的な定義
class UserMailer < ApplicationMailer
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
def password_reset(user)
+ @user = user
+ mail to: user.email, subject: "Password reset"
end
end
password_reset
メソッドの実効的な定義には、以下の内容が含まれます。
- メール本文中で使用する
@user
の内容 - メールの宛先と題名
テキストメールのテンプレート
To reset your password click the link below:
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
This will expire in two hours.
If you did not request your password to be reset, please ignore this email and
your password stay as it is.
HTMLメールのテンプレート
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
<p>This will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and
your password stay as it is.
</p>
パスワード再設定メールのプレビュー
パスワード再設定メールをプレビューできるようにする
パスワード再設定メールをプレビューできるようにするために、test/mailers/previews/user_mailer_preview.rb
の内容を書き換えていきます。
# Preview all emails at http://localhost:8080/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at http://localhost:8080/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
# Preview this email at http://localhost:8080/rails/mailers/user_mailer/password_reset
def password_reset
- UserMailer.password_reset
+ user = User.first
+ user.reset_token = User.new_token
+ UserMailer.password_reset(user)
end
end
パスワード再設定メールをプレビューする
ここまでの実装が完了すれば、test/mailers/previews/user_mailer_preview.rb
のコメント中に書かれたURLから、パスワード再設定用メールをプレビューすることができるようになります。
なお、事前にrails server
でサーバーを起動しておく必要があります。
下記はHTMLメールのプレビューです。

下記はテキストメールのプレビューです。

メール送信処理を実装した時点でのテストの結果
メール送信処理を実装した時点でのテストの結果は、以下のようになります。
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 528
Started with run options --seed 12354
FAIL["test_password_resets", PasswordResetsTest, 4.604623200000788]
test_password_resets#PasswordResetsTest (4.60s)
Expected true to be nil or false
test/integration/password_resets_test.rb:23:in `block in <class:PasswordResetsTest>'
1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04
Finished in 4.61010s
1 tests, 7 assertions, 1 failures, 0 errors, 0 skips
私の環境では、test/integration/password_resets_test.rb
の23行目には以下の記述があります。
assert_not flash.empty?
のフラッシュメッセージが定義されていないことに起因する失敗ですね。
メール送信成功時のフラッシュメッセージを追加する
メール送信成功時のフラッシュメッセージの実装を、app/controllers/password_resets_controller.rb
に追加していきます。
class PasswordResetsController < ApplicationController
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
+ flash[:info] = "Email sent with password reset instructions"
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
end
メール送信成功時のフラッシュメッセージを追加した時点でのテストの結果
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 541
Started with run options --seed 23162
FAIL["test_password_resets", PasswordResetsTest, 4.58620990000054]
test_password_resets#PasswordResetsTest (4.59s)
Expected response to be a <3XX: redirect>, but was a <204: No Content>
Response body:
test/integration/password_resets_test.rb:24:in `block in <class:PasswordResetsTest>'
1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04
Finished in 4.59156s
1 tests, 8 assertions, 1 failures, 0 errors, 0 skips
私の環境では、test/integration/password_resets_test.rb
の24行目には以下の記述があります。
assert_redirected_to root_url
「/ へリダイレクトされるべきところ、リダイレクトされていない」という失敗ですね。
メール送信成功時の処理に、/ へのリダイレクトを追加する
パスワード再設定用メールの送信が成功した場合、PasswordResetsController#create
アクションの最後は / へのリダイレクトで終了します。ここで / へのリダイレクトを追加します。
class PasswordResetsController < ApplicationController
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
+ redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
end
/ へのリダイレクトを追加した時点でのテストの結果
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 554
Started with run options --seed 2662
1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.55053s
1 tests, 8 assertions, 0 failures, 0 errors, 0 skips
ついにテストが成功しました。これにて、「有効なメールアドレスが与えられた際における、PasswordResetsController#create
の実装」ならびに「PasswordResetsController#create
の実装全体」が完了となりました。
現在までのテストが成功した時点における、パスワード再設定メールの送信用フォームに有効なメールアドレスを入力してSubmitボタンを押したときの挙動
まず、/password_resets に対してPOST
リクエストが送出され、PasswordResetsコントローラーのcreate
メソッドが開始されます。
Started POST "/password_resets" for 172.17.0.1 at 2019-12-14 06:09:11 +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 PasswordResetsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"0tty+a/E7Mp+BqIOZDaOGJQ2fH43di2VEeR3RaA9VC9MpoIb9a3BwgBNauyHYe4nBN1XC5M1TVglcsCUFNdAEQ==", "password_reset"=>"[FILTERED]", "commit"=>"Submit"}
User Load (8.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "example-2@railstutorial.org"], ["LIMIT", 1]]
(0.1ms) begin transaction
SQL (14.0ms) UPDATE "users" SET "reset_digest" = ?, "updated_at" = ? WHERE "users"."id" = ? [["reset_digest", "$2a$10$vD1xJCRoVswPj2qHsJD81OLCO3e/aviIRNCamE6OWi8TUkkG6HytS"], ["updated_at", "2019-12-14 06:09:11.878637"], ["id", 3]]
(11.2ms) commit transaction
(0.1ms) begin transaction
SQL (15.4ms) UPDATE "users" SET "updated_at" = ?, "reset_sent_at" = ? WHERE "users"."id" = ? [["updated_at", "2019-12-14 06:09:11.916190"], ["reset_sent_at", "2019-12-14 06:09:11.912111"], ["id", 3]]
(14.7ms) commit transaction
Rendering user_mailer/password_reset.html.erb within layouts/mailer
Rendered user_mailer/password_reset.html.erb within layouts/mailer (1.0ms)
Rendering user_mailer/password_reset.text.erb within layouts/mailer
Rendered user_mailer/password_reset.text.erb within layouts/mailer (0.7ms)
UserMailer#password_reset: processed outbound mail in 294.4ms
以下のはパスワード再設定用メールのヘッダー部分です。
Sent mail to example-2@railstutorial.org (3.5ms)
Date: Sat, 14 Dec 2019 06:09:12 +0000
From: noreply@example.com
To: example-2@railstutorial.org
Message-ID: <5df47c883f6f2_1c62ac8ce76084c8706b@705320d4d96d.mail>
Subject: Password reset
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_5df47c883e93a_1c62ac8ce76084c8691f";
charset=UTF-8
Content-Transfer-Encoding: 7bit
以下のログはテキスト形式のパスワード再設定用メールの内容です。
----==_mimepart_5df47c883e93a_1c62ac8ce76084c8691f
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
To reset your password click the link below:
https://localhost:3000/password_resets/bbZxxIr2r21HmbfMzsKevA/edit?email=example-2%40railstutorial.org
This will expire in two hours.
If you did not request your password to be reset, please ignore this email and
your password stay as it is.
以下のログはHTML形式のパスワード再設定用メールの内容です。
----==_mimepart_5df47c883e93a_1c62ac8ce76084c8691f
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
https://localhost:3000/password_resets/bbZxxIr2r21HmbfMzsKevA/edit?email=example-2%40railstutorial.org
<p>This will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and
your password stay as it is.
</p>
</body>
</html>
----==_mimepart_5df47c883e93a_1c62ac8ce76084c8691f--
パスワード再設定用メールの内容についてのログは以上です。
Redirected to http://localhost:8080/
Completed 302 Found in 477ms (ActiveRecord: 63.7ms)
/password_resets に対するPOST
リクエスト(すなわちPasswordResetsコントローラーのcreate
アクション)が、「302 FOUND」というステータスコードを返し、/ に対するリダイレクトによって完了しました。
以降は、リダイレクト後の、/ に対するGET
リクエストのログです。
Started GET "/" for 172.17.0.1 at 2019-12-14 06:09:12 +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 StaticPagesController#home as HTML
Rendering static_pages/home.html.erb within layouts/application
Rendered static_pages/home.html.erb within layouts/application (18.3ms)
Rendered layouts/_rails_default.erb (220.1ms)
Rendered layouts/_shim.html.erb (0.4ms)
Rendered layouts/_header.html.erb (1.0ms)
Rendered layouts/_footer.html.erb (0.7ms)
Completed 200 OK in 377ms (Views: 359.5ms | ActiveRecord: 0.0ms)
/ の描画が、「200 OK」というステータスコードとともに正常に完了していますね。
実際にWebブラウザでパスワード再設定メールの送信用フォームにSubmitしてみる
まずは、パスワード再設定メールの送信用フォームを表示します。

有効なメールアドレスを入力し、「Submit」ボタンを押すと、以下の画面が表示されます。

確かに「Email sent with password reset instructions」というフラッシュメッセージが表示されていますね。