何が起こっているか
第12章、「有効なメールアドレスをリクエスト内容に含めた状態で、/password_resets に対するPOST
リクエストを行った場合の実装」の途中で事態が発生しました。
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[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
end
この状態で、以下のテストを実行します。
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'
# メールアドレスが有効
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.deliveries.size
assert_not flash.empty?
assert_redirected_to root_url
end
end
すると、結果は以下のようになります。
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 379
Started with run options --seed 13692
FAIL["test_password_resets", PasswordResetsTest, 4.077262900000278]
test_password_resets#PasswordResetsTest (4.08s)
Expected response to be a <3XX: redirect>, but was a <204: No Content>
Response body:
test/integration/password_resets_test.rb:22:in `block in <class:PasswordResetsTest>'
1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04
Finished in 4.08408s
1 tests, 7 assertions, 1 failures, 0 errors, 0 skips
上述の失敗が発生しているのは、以下のテストコードに対してです。
assert_redirected_to root_url
…あれ?21行目のテストが失敗していません。「メールアドレスが有効な場合の処理において、フラッシュメッセージの定義がない」はずの状態であるにもかかわらずです。
assert_not flash.empty?
debugger
で何が起こっているか確認してみる
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'
# メールアドレスが有効
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.deliveries.size
+ debugger
assert_not flash.empty?
assert_redirected_to root_url
end
end
結果は以下のようになります。
# rails test test/integration/password_resets_test.rb
Running via Spring preloader in process 392
Started with run options --seed 40130
/0: [=---=---=---=---=---=---=---=---=---=-] 0% Time: 00:00:00, ETA: ??:??:??
[16, 25] in /var/www/sample_app/test/integration/password_resets_test.rb
16: assert_template 'password_resets/new'
17: # メールアドレスが有効
18: post password_resets_path, params: { password_reset: { email: @user.email} }
19: assert_not_equal @user.reset_digest, @user.reload.reset_digest
20: assert_equal 1, ActionMailer::Base.deliveries.size
21: debugger
=> 22: assert_not flash.empty?
23: assert_redirected_to root_url
24: end
25: end
(byebug) flash
#<ActionDispatch::Flash::FlashHash:0x00005629bc295e50 @discard=#<Set: {"danger"}>, @flashes={"danger"=>"Email address not found"}, @now=nil>
「メールアドレスが無効な場合のフラッシュメッセージの内容が、メールアドレスが有効な場合のフラッシュメッセージのテストのときまで残ってしまっている」という事態のようです。
このような不具合の原因は、「Railsの仕様によるフラッシュメッセージの表示期間のルール」であろうと推測されます。
-
flash.now
の場合、フラッシュメッセージの表示期間は現在のリクエスト限り -
flash
の場合、フラッシュメッセージの表示期間は次のリクエストまで
メールアドレスが有効な場合のフラッシュメッセージの定義を忘れても、このテストは成功する
例えば以下のような実装の場合、上記のテストは成功するのでしょうか。
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
# 成功時のフラッシュメッセージの定義がない
redirect_to root_url
else
flash[: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 476
Started with run options --seed 31973
1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.44346s
1 tests, 7 assertions, 0 failures, 0 errors, 0 skips
なんということでしょう。テストが成功してしまいました。
この状態で、パスワード再設定メールの送信用フォームに無効なメールアドレスを入力してからの動作をブラウザで確認してみる
まず、ブラウザでパスワード再設定メールの送信用フォームに無効なメールアドレスを入力し、Submitボタンを押してみます。
パスワード再設定メールの送信用フォームが、「Email address not found」というフラッシュメッセージが出力された状態で出力されます。ここまでは正常な動作です。
続いて、そのまま「Home」リンクをクリックしてみます。
「Email address not found」というフラッシュメッセージが出力された状態のままです。これは異常な動作ですね。
何が問題だったのか
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
redirect_to root_url
else
- flash[:danger] = "Email address not found"
+ flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
end
メールアドレスが無効の際の処理で、flash
とflash.now
を誤用していました。
テストコードのリファクタリング
flash
とflash.now
の誤用を検出できるテスト
「flash
とflash.now
の誤用」も、テストで検出されるべきバグですね。となると、テストコードのリファクタリングが可能、ということになります。
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.deliveries.size
assert_not flash.empty?
assert_redirected_to root_url
end
end
「assert_template 'password_resets/new'
の後にGET
が呼び出された時点でフラッシュメッセージが破棄される」というのが正しい実装のはずです。GET
が呼び出された後にフラッシュメッセージが空でないなら何かバグがある…ということになります。
このテストは、今回のようなバグを検出できるか
test/integration/password_resets_test.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
redirect_to root_url
else
flash[: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 502
Started with run options --seed 41860
FAIL["test_password_resets", PasswordResetsTest, 2.5368660999993153]
test_password_resets#PasswordResetsTest (2.54s)
Expected false to be truthy.
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.53875s
1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
テストは無事(?)失敗しました。test/integration/password_resets_test.rb
の18行目で、「真であるべき式が偽になっている」という理由でテストが失敗しています。
assert flash.empty?
私の環境では、test/integration/password_resets_test.rb
の18行目は上述のようなコードになります。正しい対象にテストを行えている、と判断できます。