0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Railsチュートリアル 第12章 パスワードの再設定 - ソースコードに混入したバグと、テストに対する一つのリファクタリング

Posted at

何が起こっているか

第12章、「有効なメールアドレスをリクエスト内容に含めた状態で、/password_resets に対するPOSTリクエストを行った場合の実装」の途中で事態が発生しました。

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
      #TODO: フラッシュメッセージの定義とルートへのリダイレクト
    else
      flash[:danger] = "Email address not found"
      render 'new'
    end
  end

  def edit
  end
end

この状態で、以下のテストを実行します。

test/integration/password_resets_test.rb
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

上述の失敗が発生しているのは、以下のテストコードに対してです。

test/integration/password_resets_test.rb(22行目)
assert_redirected_to root_url

…あれ?21行目のテストが失敗していません。「メールアドレスが有効な場合の処理において、フラッシュメッセージの定義がない」はずの状態であるにもかかわらずです。

test/integration/password_resets_test.rb(21行目)
assert_not flash.empty?

debuggerで何が起こっているか確認してみる

test/integration/password_resets_test.rb
  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の場合、フラッシュメッセージの表示期間は次のリクエストまで

メールアドレスが有効な場合のフラッシュメッセージの定義を忘れても、このテストは成功する

例えば以下のような実装の場合、上記のテストは成功するのでしょうか。

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
      # 成功時のフラッシュメッセージの定義がない
      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ボタンを押してみます。

スクリーンショット 2019-12-14 10.58.36.png

パスワード再設定メールの送信用フォームが、「Email address not found」というフラッシュメッセージが出力された状態で出力されます。ここまでは正常な動作です。

続いて、そのまま「Home」リンクをクリックしてみます。

スクリーンショット 2019-12-14 10.58.47.png

「Email address not found」というフラッシュメッセージが出力された状態のままです。これは異常な動作ですね。

何が問題だったのか

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
        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

メールアドレスが無効の際の処理で、flashflash.nowを誤用していました。

テストコードのリファクタリング

flashflash.nowの誤用を検出できるテスト

flashflash.nowの誤用」も、テストで検出されるべきバグですね。となると、テストコードのリファクタリングが可能、ということになります。

test/integration/password_resets_test.rb
  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を上述のように記述した場合、以下の実装に対してテストを行うと、結果はどうなるでしょうか。

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
      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行目で、「真であるべき式が偽になっている」という理由でテストが失敗しています。

test/integration/password_resets_test.rb(18行目)
assert flash.empty?

私の環境では、test/integration/password_resets_test.rbの18行目は上述のようなコードになります。正しい対象にテストを行えている、と判断できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?