Railsチュートリアル 第13章 ユーザーのマイクロポスト - 画像アップローダーのテストに存在する一つの不具合と、その解消策

# 画像アップローダーのテスト（オリジナル）

Railsチュートリアル本文においては、第13章の[演習 - 基本的な画像アップロード](https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-exercises_basic_image_upload)に登場するテストです。

同演習で`FILL_IN`となっている箇所を埋めると、テストの内容は以下のようになります。

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

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:rhakurei)
    @other_user = users(:skomeiji)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'img.gravatar'
    assert_select 'h1', @user.name
    assert_select 'span', /#{@user.microposts.count}/
    assert_select 'form[action="/microposts"]'
    assert_select 'textarea'
    assert_select 'div.pagination'
    assert_select 'input[type="file"]'
    # 無効な送信
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/lgtm3.png', 'image/png')
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content, picture: picture } }
    end
    assert assigns(:micropost).picture?
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    # 投稿を削除する
    assert_select 'a', text: 'delete'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # 違うユーザーのプロフィールにアクセス（削除リンクがないことの確認）
    get user_path(users(:mkirisame))
    assert_select 'a', text: 'delete', count: 0
  end

  # ...略
end
```

# このテストに関する一つの不具合

実は、上記のテストには一つ不具合があります。

例えば、以下のように`app/views/microposts/_micropost.html.erb`から画像表示部分の実装を取り除くと、テストの結果はどうなるでしょうか。

```diff:app/views/microposts/_micropost.html.erb
  <li id="micropost-<%= micropost.id %>">
    <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
    <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
    <span class="content">
      <%= micropost.content %>
-     <%= image_tag micropost.picture.url if micropost.picture? %>
    </span>
    <span class="timestamp">
      Posted <%= time_ago_in_words(micropost.created_at) %> ago.
      <% if current_user?(micropost.user) %>
        <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %>
      <% end %>
    </span>
  </li>
```

以下のように**テストが成功してしまうのです。**

```
# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 15428
Started with run options --seed 30155

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

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

「画像を投稿したのに、投稿した画像がフィードに表示されていない」というのは、テストで検出できるべきバグであり、それを検出できないのはテストの不具合です。早速、テストに存在する当該不具合を解消していきましょう。

# 画像アップローダーのテストに存在する不具合を解消する

`test/integration/microposts_interface_test.rb`を以下のように変更するのが一つの方法です（他にも方法はあるかもしれません）。

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

  class MicropostsInterfaceTest < ActionDispatch::IntegrationTest
    def setup
      @user = users(:rhakurei)
      @other_user = users(:skomeiji)
    end

    test "micropost interface" do
      log_in_as(@user)
      get root_path
      ...略
      # 有効な送信
      content = "This micropost really ties the room together"
      picture = fixture_file_upload('test/fixtures/lgtm3.png', 'image/png')
      assert_difference 'Micropost.count', 1 do
        post microposts_path, params: { micropost: { content: content, picture: picture } }
      end
-     assert assigns(:micropost).picture?
      assert_redirected_to root_url
      follow_redirect!
      assert_match content, response.body
+     assert_select "img[src*='#{picture.original_filename}']"
      # 投稿を削除する
      ...略
    end

    ...略
  end
```

## 新たなテストのポイント

ポイントは以下の`assert_select`です。

```ruby:
assert_select "img[src*='#{picture.original_filename}']"
```

`picture.original_filename`というのは、「アップロードしたファイルのローカルにおけるファイル名」と考えてかまいません[^original_filenameについて]。また、CSSセレクタで部分一致検索を行うためには、`*=`という演算子を用います。

結果、上記`assert_select`は、「`src`属性に、アップロードしたファイルのローカルにおけるファイル名を含む`img`要素が存在すること」をチェックします。

また、Rails5で非推奨とされている`assigns`を使わない形となり、Webアプリケーションのテストの書き方としてより望ましいとされる「Request Specなテストの書き方」に近づけることができます。

[^original_filenameについて]: より厳密には、「CarrierWaveデフォルトの状態だと日本語の文字が"__"に置き換えられてしまう」等の制限があるようなのですが、本記事で扱う内容の範囲外となるため、そのあたりの説明は割愛します。

# 実装を変更したテストが、本当に正しい対象へのテストなのか

## マイクロポストに関連付けられた画像を表示する部分が欠落したビューに対してテストを実行する

`app/views/microposts/_micropost.html.erb`の内容が以下のようになっている場合、テストの結果はどうなるでしょうか。

```erb:app/views/microposts/_micropost.html.erb（不具合あり）
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>
```

```
# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 15608
Started with run options --seed 24115

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 4.238321199998609]
 test_micropost_interface#MicropostsInterfaceTest (4.24s)
        Expected at least 1 element matching "img[src*='lgtm3.png']", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:34:in `block in <class:MicropostsInterfaceTest>'

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

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

```
Expected at least 1 element matching "img[src*='lgtm3.png']", found 0..
Expected 0 to be >= 1.
test/integration/microposts_interface_test.rb:34
```

以上のようなメッセージが出てテストが失敗しています。「`lgtm3.png`という文字列を含む`src`属性を持つ`img`要素が、1つ以上なければならないのに、1つも存在しない」という趣旨のメッセージですね。

私の環境では、`test/integration/microposts_interface_test.rb`の34行目は以下内容になっています。

```ruby:test/integration/microposts_interface_test.rb（34行目）
assert_select "img[src*='#{picture.original_filename}']"
```

確かに想定通りの形でテストが失敗しているといえます。

## マイクロポストに関連付けられた画像を表示する部分を正しく実装したビューに対してテストを実行する

`app/views/microposts/_micropost.html.erb`の中身を、今度は以下のようにしてテストを実行してみましょう。

```erb:app/views/microposts/_micropost.html.erb（不具合修正後）
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>
```

```
# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 15621
Started with run options --seed 58972

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

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

今度こそテストが無事成功しました。

# 関連リンク

「CSSにおける部分一致検索」については、以下の記事を参考にさせていただきました。ありがとうございます。

* [【CSS】実はこんなに便利！cssのセレクタ指定方法をちょっとだけ掘り起こしてみた | バシャログ。](http://bashalog.c-brains.jp/12/03/06-235258.php#case02)