Railsチュートリアル 第14章 ユーザーをフォローする - 演習「フォローをテストする」 - FollowingTestの問題点と、その改良

# 前提となるテストの内容

Railsチュートリアル本文、[リスト 14.40](https://railstutorial.jp/chapters/following_users?version=5.1#code-follow_button_tests)記載のテストコードです。

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

class FollowingTest < ActionDispatch::IntegrationTest
  def setup
    @user  = users(:rhakurei)
    @other = users(:mkirisame)
    log_in_as(@user)
  end

  # ...略

  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, params: { followed_id: @other.id }
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship), xhr: true
    end
  end
end

```

# `RelationshipsController#create`において、`format.js`以下の行が欠落していても上記テストが落ちない理由

前提として、`RelationshipsController#create`の実装は以下のようになっています。

```ruby:app/controllers/relationships_controller.rb#create
def create
  @user = User.find(params[:followed_id])
  current_user.follow(@user)
  respond_to do |format|
    format.html { redirect_to @user }
    format.js
  end
end
```

`create`メソッド内2行目の時点で、`follow`メソッドの処理に問題がなければ、`@user.following.count`の数は1増えます。

ここで、`format.js`以下の行が欠落している場合、`RelationshipsController#create`は、「Ajaxによる呼び出しであっても、非Ajax（通常のリダイレクトを伴う呼び出し）として以降の処理を行います。となると、「`format.js`以下の行が欠落していても、`format.html`以下の行さえあれば、とりあえず当該テストは通ってしまう」という動作になるのです。

# 現状の実装で、テスト「should follow a user with Ajax」はどんな場合に落ちるのか

Railsチュートリアル本文の[リスト 14.40](https://railstutorial.jp/chapters/following_users?version=5.1#code-follow_button_tests)において、テスト「should follow a user with Ajax」のソースコードは、以下のようになっています。

```ruby:
test "should follow a user with Ajax" do
  assert_difference '@user.following.count', 1 do
    post relationships_path, xhr: true, params: { followed_id: @other.id }
  end
end
```

「`RelationshipsController#create`において、`format.js`以下の行が欠落していてもこのテストが落ちない」というのであれば、どのような場合にこのテストは落ちるのでしょうか。

一つの答えは、「`app/views/relationships`ディレクトリに、`create.js.erb`がなくて`create.css.erb`だけが存在する場合」です。この場合、当該テストは以下のようなエラーで落ちることになります。

```
# rails test test/integration/following_test.rb
Running via Spring preloader in process 507
Started with run options --seed 49836

ERROR["test_should_follow_a_user_with_Ajax", FollowingTest, 4.884168800000225]
 test_should_follow_a_user_with_Ajax#FollowingTest (4.88s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: RelationshipsController#create is missing a template for this request format and variant.
        
        request.formats: ["text/javascript", "text/html", "application/xml", "*/*"]
        request.variant: []
            test/integration/following_test.rb:37:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:36:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.96005s
6 tests, 13 assertions, 0 failures, 1 errors, 0 skips
```

`ActionController::UnknownFormat`エラーでテストが落ちていますね。

# `test/integration/following_test.rb`の、より望ましいであろう実装

ビューによりレンダリングされる内容に対してテストを行うのであれば、以下のような実装がより望ましいかと思います。

```diff:test/integration/following_test.rb
  require 'test_helper'

  class FollowingTest < ActionDispatch::IntegrationTest
    def setup
      @user  = users(:rhakurei)
      @other = users(:mkirisame)
      log_in_as(@user)
    end

...略

    test "should follow a user the standard way" do
      assert_difference '@user.following.count', 1 do
        post relationships_path, params: { followed_id: @other.id }
      end
+     assert_redirected_to @other
    end

    test "should follow a user with Ajax" do
      assert_difference '@user.following.count', 1 do
        post relationships_path, xhr: true, params: { followed_id: @other.id }
      end
+     assert_match 'Unfollow', @response.body
    end

    test "should unfollow a user the standard way" do
      @user.follow(@other)
      relationship = @user.active_relationships.find_by(followed_id: @other.id)
      assert_difference '@user.following.count', -1 do
        delete relationship_path(relationship)
      end
+     assert_redirected_to @other
    end

    test "should unfollow a user with Ajax" do
      @user.follow(@other)
      relationship = @user.active_relationships.find_by(followed_id: @other.id)
      assert_difference '@user.following.count', -1 do
        delete relationship_path(relationship), xhr: true
      end
+     assert_match 'Follow', @response.body
    end
  end
```

上記テストの追加内容は以下の通りです。

* 非Ajaxの場合、`@other`の内容たるユーザーのプロフィールページにリダイレクトされることをテストする
* Ajaxの場合、以下の処理が行われることをテストする
    * Followボタンが押された場合、「Unfollow」というキャプションを持つボタンを生成するJavaScriptコードが返されること
    * Unfollowボタンが押された場合、「Follow」というキャプションを持つボタンを生成するJavaScriptコードが返されること

# 「より望ましいであろう実装」のテスト結果

## 「should follow a user the standard way」のテストが落ちるパターン

以下のように、「`RelationshipsController#create`において、`format.html`以下の行が欠落している場合」、「should follow a user the standard way」のテストが落ちることが期待されます。

```diff:app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
-     format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end
```

実際にテスト「should follow a user the standard way」を実行すると、結果は以下のようになります。

```
# rails test test/integration/following_test.rb
Running via Spring preloader in process 664
Started with run options --seed 25360

ERROR["test_should_follow_a_user_the_standard_way", FollowingTest, 4.109552399999302]
 test_should_follow_a_user_the_standard_way#FollowingTest (4.11s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:7:in `create'
            test/integration/following_test.rb:30:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:29:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.22645s
6 tests, 19 assertions, 0 failures, 1 errors, 0 skips
```

## 「should follow a user with Ajax」のテストが落ちるパターン

以下のように、「`RelationshipsController#create`において、`format.js`以下の行が欠落している」という場合、「should follow a user with Ajax」のテストが落ちることが期待されます。

```diff:app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
-     format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end
```

実際にテスト「should follow a user with Ajax」を実行すると、結果は以下のようになります。

```
# rails test test/integration/following_test.rb
Running via Spring preloader in process 677
Started with run options --seed 49139

 FAIL["test_should_follow_a_user_with_Ajax", FollowingTest, 1.8547959999996237]
 test_should_follow_a_user_with_Ajax#FollowingTest (1.85s)
        Expected /Unfollow/ to match "Turbolinks.clearCache()\nTurbolinks.visit(\"http://www.example.com/users/391532587\", {\"action\":\"replace\"})".
        test/integration/following_test.rb:39:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.41998s
6 tests, 22 assertions, 1 failures, 0 errors, 0 skips
```

## 「should unfollow a user the standard way」のテストが落ちるパターン

以下のように、「`RelationshipsController#destroy`において、`format.html`以下の行が欠落している場合」、「should follow a user the standard way」のテストが落ちることが期待されます。

```diff:app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
-     format.html { redirect_to @user }
      format.js
    end
  end
end
```

実際にテスト「should unfollow a user the standard way」を実行すると、結果は以下のようになります。

```
# rails test test/integration/following_test.rb
Running via Spring preloader in process 690
Started with run options --seed 23512

ERROR["test_should_unfollow_a_user_the_standard_way", FollowingTest, 3.8890324999993027]
 test_should_unfollow_a_user_the_standard_way#FollowingTest (3.89s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:16:in `destroy'
            test/integration/following_test.rb:46:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:45:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.18608s
6 tests, 19 assertions, 0 failures, 1 errors, 0 skips
```

## 「should unfollow a user with Ajax」のテストが落ちるパターン

以下のように、「`RelationshipsController#destroy`において、`format.js`以下の行が欠落している場合」、「should unfollow a user with Ajax」のテストが落ちることが期待されます。

```diff:app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
-     format.js
    end
  end
end
```

実際にテスト「should unfollow a user with Ajax」を実行すると、結果は以下のようになります。

```
# rails test test/integration/following_test.rb
Running via Spring preloader in process 703
Started with run options --seed 27626

 FAIL["test_should_unfollow_a_user_with_Ajax", FollowingTest, 1.7373090000000957]
 test_should_unfollow_a_user_with_Ajax#FollowingTest (1.74s)
        Expected /Follow/ to match "Turbolinks.clearCache()\nTurbolinks.visit(\"http://www.example.com/users/391532587\", {\"action\":\"replace\"})".
        test/integration/following_test.rb:57:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.29870s
6 tests, 22 assertions, 1 failures, 0 errors, 0 skips
```

## ソースコードの実装に問題がない場合

最後に、`app/controllers/relationships_controller.rb`の内容が以下の通り問題なく記述されている場合、テストの結果はどうなるでしょうか。

```ruby:app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end
```

実際にテストを実行してみます。

```
# rails test test/integration/following_test.rb
Running via Spring preloader in process 716
Started with run options --seed 34373

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.64541s
6 tests, 22 assertions, 0 failures, 0 errors, 0 skips
```

無事テストが通りました。というわけで、テストの内容に問題はないようです。