Railsチュートリアル 第11章 アカウントの有効化 - /users と /users/:id に対する統合テスト

# 何について書いているか

Railsチュートリアル第11章、[演習 - 有効化のテストとリファクタリング](https://railstutorial.jp/chapters/account_activation?version=5.1#sec-exercises_activation_test_and_refactoring)のうち、3番目の演習内容について書いています。

> ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。 

# 統合テストの作成

テストの名前は`users_activation`とします。

```
# rails generate integration_test users_activation
Running via Spring preloader in process 14164
      invoke  test_unit
      create    test/integration/users_activation_test.rb
```

# 当該統合テストの前提となる実装を行う

## fixtureに、有効でないユーザーを定義する

現状のfixtureには、有効でないユーザーは定義されていません。テストを実行する前に、まずは有効でないユーザーがRDB上に存在するよう、fixtureにユーザーを追加する必要があります。

fixtureで有効でないユーザーを生成する場合、例えば以下のようなYAMLが必要となります。

```yaml:有効でないユーザーを生成するYAMLの例
not_activated:
  name: User Not activated
  email: example_foobar@example.org
  password_digest: <%= User.digest('password') %>
  activated: false
  activated_at: nil
```

ポイントは以下です。

* `activated`が`false`であること
* `activated_at`が`nil`であること

## `UsersActivationTest#setup`メソッド

実際のテストに先がけて、テストで使うユーザー情報をインスタンス変数に格納する必要があります。そうした処理は、テストの`setup`メソッドで行うのでしたね。

```ruby:UsersActivationTest#setup
  def setup
    @admin = users(:rhakurei)
    @user_not_activated = users(:not_activated)
  end
```

以下の処理を行っています。

* 管理ユーザーを`@admin`として定義する
    * /users を表示するために、有効なユーザーが必要となる
* 有効でないユーザーを`@user_not_activated`として定義する

# 必要なテストの内容

必要なテストの内容は以下になります。

* /users で表示されるユーザー一覧に、有効でないユーザーが表示されないこと
* /users/:id で有効でないユーザーを表示しようとしたときに、/ にリダイレクトされること

## /users で表示されるユーザー一覧に、有効でないユーザーが表示されないこと

名前は「index should not include not activated user」としました

```ruby:
test "index should not include not activated user" do
  log_in_as(@admin)
  get users_path
  assert_select 'a[href=?]', user_path(@user_not_activated), count: 0
end
```

動作内容は以下となります。

1. 管理ユーザーでログインする
2. `users_path`に対し、`GET`リクエストを送信する
3. 有効でないユーザーのプロフィールページへのリンクが表示されていなければテスト成功

## /users/:id で有効でないユーザーを表示しようとしたときに、/ にリダイレクトされること

名前は「show user who is not activated should redirect to root」としました。

```ruby:
test "show user who is not activated should redirect to root" do
  get user_path(@user_not_activated)
  assert_redirected_to root_url
end
```

動作内容は以下となります。

1. 有効でないユーザーの`user_path`に対し、`GET`リクエストを送信する
2. / にリダイレクトされればテスト成功

## `test/integration/users_activation_test.rb`の全体像

`test/integration/users_activation_test.rb`の全体像は以下のようになります。

```ruby:test/integration/users_activation_test.rb
require 'test_helper'

class UsersActivationTest < ActionDispatch::IntegrationTest
  def setup
    @admin = users(:rhakurei)
    @user_not_activated = users(:not_activated)
  end

  test "index should not include not activated user" do
    log_in_as(@admin)
    get users_path
    assert_select 'a[href=?]', user_path(@user_not_activated), count: 0
  end

  test "show user who is not activated should redirect to root" do
    get user_path(@user_not_activated)
    assert_redirected_to root_url
  end
end
```

# この時点でテストは成功する

この時点で、`test/integration/users_activation_test.rb`を対象にテストを実行すると、問題なく成功します。

```
# rails test test/integration/users_activation_test.rb
Running via Spring preloader in process 14245
Started with run options --seed 11346

  2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.75769s
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
```

# 逆にどのようなバグがあったらテストが失敗するのか

## /users で表示されるユーザー一覧に、有効でないユーザーが表示される場合

仮に、`UsersController#index`の実装が以下のようになっていたら、テストの結果はどうなるでしょう。

```ruby:UsersController#index
def index
  @users = User.paginate(page: params[:page])
end
```

以下のようなメッセージが表示されてテストが失敗します。

```
# rails test test/integration/users_activation_test.rb
Running via Spring preloader in process 14284
Started with run options --seed 11740

  /0: [=---=---=---=---=---=---=---=---=---=-] 0% Time: 00:00:00,  ETA: ??:??:??
[8, 17] in /var/www/sample_app/test/integration/users_activation_test.rb
    8: 
    9:   test "index should not include not activated user" do
   10:     log_in_as(@admin)
   11:     get users_path
   12:     debugger
=> 13:     assert_select 'a[href=?]', user_path(@user_not_activated), count: 0
   14:   end
   15:   ...略
(byebug) user_path(@user_not_activated)
"/users/826701971"
 FAIL["test_index_should_not_include_not_activated_user", UsersActivationTest, 11.721780299965758]
 test_index_should_not_include_not_activated_user#UsersActivationTest (11.72s)
        Expected exactly 0 elements matching "a[href="/users/826701971"]", found 2..
        Expected: 0
          Actual: 2
        test/integration/users_activation_test.rb:13:in `block in <class:UsersActivationTest>'

  2/2: [===================================] 100% Time: 00:00:11, Time: 00:00:11

Finished in 11.76151s
2 tests, 2 assertions, 1 failures, 0 errors, 0 skips
```

/users/826701971 というのは、`user_path(@user_not_activated)`のことです（わかりやすくするために、ここでは`assert_select`の前に`debugger`で確認しています）。

なお、`a[href="/users/826701971"]`の数の「2」というのは、`GET`用のリンクと`DELETE`用のリンクで2つ、ということです。ログインユーザーが管理ユーザーでない場合、ここは「1」となるはずですね。

### /users に有効でないユーザーが表示される - どう修正すればいいか

/index で有効なユーザーのみを表示するようにするためには、`@users`を定義するメソッドチェーンに`where(activated: true)`が必要なのでしたね。

```diff:UsersController#index
  def index
-   @users = User.paginate(page: params[:page])
+   @users = User.where(activated: true).paginate(page: params[:page])   
  end
```

今度こそテストは成功するはずです。

```
# rails test test/integration/users_activation_test.rb
Running via Spring preloader in process 14323
Started with run options --seed 3334

  2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.32350s
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
```

## /users/:id で有効でないユーザーを表示しようとしたときに、/ にリダイレクトされない場合

仮に、`UsersController#show`の実装が以下のようになっていたら、テストの結果はどうなるでしょう。

```ruby:UsersController#show
def show
  @user = User.find(params[:id])
end
```

以下のようなメッセージが表示されてテストが失敗します。

```
# rails test test/integration/users_activation_test.rb
Running via Spring preloader in process 14310
Started with run options --seed 41248

 FAIL["test_show_user_who_is_not_activated_should_redirect_to_root", UsersActivationTest, 2.460988399980124]
 test_show_user_who_is_not_activated_should_redirect_to_root#UsersActivationTest (2.46s)
        Expected response to be a <3XX: redirect>, but was a <200: OK>
        test/integration/users_activation_test.rb:17:in `block in <class:UsersActivationTest>'

  2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.07865s
2 tests, 2 assertions, 1 failures, 0 errors, 0 skips
```

「リダイレクトされるべき場面でリダイレクトされていない」という趣旨のメッセージですね。

### 有効でないユーザーを表示しようとしたときに、/ にリダイレクトされない - どう修正すればいいか

`UsersController#show`を以下のように修正します。

```diff:UsersController#show
  def show
    @user = User.find(params[:id])
+   redirect_to root_url unless @user.activated?
  end
```

今度こそテストは成功するはずです。

```
# rails test test/integration/users_activation_test.rb
Running via Spring preloader in process 14336
Started with run options --seed 35088

  2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.52763s
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
```

# `test/integration/users_index_test.rb`にも修正が必要だった

## テスト失敗の内容

fixtureに有効でないユーザーを含むようにすると、全体のテストが成功しなくなります。

```
# rails test
Running via Spring preloader in process 14362
Started with run options --seed 17156

 FAIL["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 6.323125800001435]
 test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (6.32s)
        Expected at least 1 element matching "a[href="/users/826701971"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_index_test.rb:16:in `block (2 levels) in <class:UsersIndexTest>'
        test/integration/users_index_test.rb:15:in `block in <class:UsersIndexTest>'

  46/46: [=================================] 100% Time: 00:00:07, Time: 00:00:07

Finished in 7.62021s
46 tests, 188 assertions, 1 failures, 0 errors, 0 skips
```

## 対応するテスト

`test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest`に対応するソースコードは、`test/integration/users_index_test.rb`の以下の部分ですね。

```ruby:test/integration/users_index_test.rb（抜粋）
test "index as admin including pagination and delete links" do
  log_in_as(@admin)
  get users_path
  assert_template 'users/index'
  assert_select 'div.pagination'
  first_page_of_users = User.paginate(page: 1)
  first_page_of_users.each do |user|
    assert_select 'a[href=?]', user_path(user), text: user.name
    unless user == @admin
      assert_select 'a[href=?]', user_path(user), text: 'delete'
    end
  end
  assert_difference 'User.count', -1 do
    delete user_path(@non_admin)
  end
end
```

## 原因と修正

原因は以下の行です。1ページ目に表示されるユーザーの組を、「有効でないユーザーを含む」という形で定義してしまっているためですね。

```ruby:
first_page_of_users = User.paginate(page: 1)
```

当該実装は、以下のように修正する必要があります。「1ページ目に表示されるユーザーの組に、有効なユーザーのみを含むようにする」という実装ですね。

```diff:
- first_page_of_users = User.paginate(page: 1)
+ first_page_of_users = User.where(activated: true).paginate(page: 1)
```

全体としては以下の修正内容となります。

```diff:ruby:test/integration/users_index_test.rb（抜粋）
  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
-   first_page_of_users = User.paginate(page: 1)
+   first_page_of_users = User.where(activated: true).paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end
```

## 今度こそ全体のテストが成功する

当該修正を適用すれば、今度こそ全体のテストが成功するようになります。

```
# rails test
Running via Spring preloader in process 14401
Started with run options --seed 21435

  46/46: [=================================] 100% Time: 00:00:07, Time: 00:00:07

Finished in 7.06875s
46 tests, 197 assertions, 0 failures, 0 errors, 0 skips
```