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

# 何が起こっているか

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

```ruby: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
```

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

```ruby: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
```

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

```ruby:test/integration/password_resets_test.rb（22行目）
assert_redirected_to root_url
```

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

```ruby:test/integration/password_resets_test.rb（21行目）
assert_not flash.empty?
```

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

```diff: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`の場合、フラッシュメッセージの表示期間は次のリクエストまで

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

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

```ruby: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ボタンを押してみます。

<img width="1171" alt="スクリーンショット 2019-12-14 10.58.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/c6d29263-011e-d191-b466-657c2ddbc80d.png">

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

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

<img width="1171" alt="スクリーンショット 2019-12-14 10.58.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/03084a37-5c4f-d795-bf84-87fcc4d769a4.png">

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

# 何が問題だったのか

```diff: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
```

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

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

## `flash`と`flash.now`の誤用を検出できるテスト

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

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

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

```ruby:test/integration/password_resets_test.rb（18行目）
assert flash.empty?
```

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