紙一重の積み重ね

アラフォーのエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【14章】Ruby on Railsチュートリアル演習まとめ&解答例【14.2 Relationshipモデル】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 14章 14.2 Relationshipモデルの演習まとめ&解答例です。

個人の解答例なので、誤りがあればご指摘ください。

動作環境

  • cloud9
  • ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
  • Rails 5.0.0.1

14.2.1 フォローのサンプルデータ

本章での学び

【seeds】サンプルデータにfollowing/followerの関係性を追加する

rails db:seedを使って、DBにサンプルデータを登録できるようにする。

  • 最初のユーザは、3〜51までのユーザをフォローする
  • ユーザ4〜41は、最初のユーザをフォローする
# リレーションシップ
users = User.all
user = User.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }

【DB】DBのリセット

yokoyan:~/workspace/sample_app (following-users) $ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20170417215343 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0026s
== 20170417215343 CreateUsers: migrated (0.0028s) =============================

== 20170423125906 AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0016s
== 20170423125906 AddIndexToUsersEmail: migrated (0.0017s) ====================

== 20170424041610 AddPasswordDigestToUsers: migrating =========================
-- add_column(:users, :password_digest, :string)
   -> 0.0021s
== 20170424041610 AddPasswordDigestToUsers: migrated (0.0022s) ================

== 20170514215307 AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0011s
== 20170514215307 AddRememberDigestToUsers: migrated (0.0013s) ================

== 20170614215124 AddAdminToUsers: migrating ==================================
-- add_column(:users, :admin, :boolean, {:default=>false})
   -> 0.0015s
== 20170614215124 AddAdminToUsers: migrated (0.0016s) =========================

== 20170618221238 AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
   -> 0.0035s
-- add_column(:users, :activated, :boolean, {:dafault=>false})
   -> 0.0009s
-- add_column(:users, :activated_at, :datetime)
   -> 0.0009s
== 20170618221238 AddActivationToUsers: migrated (0.0056s) ====================

== 20170703042006 AddResetToUsers: migrating ==================================
-- add_column(:users, :reset_digest, :string)
   -> 0.0019s
-- add_column(:users, :reset_sent_at, :datetime)
   -> 0.0006s
== 20170703042006 AddResetToUsers: migrated (0.0027s) =========================

== 20170719214241 CreateMicroposts: migrating =================================
-- create_table(:microposts)
   -> 0.0026s
-- add_index(:microposts, [:user_id, :created_at])
   -> 0.0014s
== 20170719214241 CreateMicroposts: migrated (0.0042s) ========================

== 20170820040443 AddPictureToMicroposts: migrating ===========================
-- add_column(:microposts, :picture, :string)
   -> 0.0015s
== 20170820040443 AddPictureToMicroposts: migrated (0.0016s) ==================

== 20170823121831 CreateRelationships: migrating ==============================
-- create_table(:relationships)
   -> 0.0013s
-- add_index(:relationships, :follower_id)
   -> 0.0008s
-- add_index(:relationships, :followed_id)
   -> 0.0012s
-- add_index(:relationships, [:follower_id, :followed_id], {:unique=>true})
   -> 0.0023s
== 20170823121831 CreateRelationships: migrated (0.0061s) =====================

【DB】初期データの投入

yokoyan:~/workspace/sample_app (following-users) $ rails db:seed

演習1

コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。

38人にフォローされていることを確認。

yokoyan:~/workspace/sample_app (following-users) $ rails console
Running via Spring preloader in process 7369
Loading development environment (Rails 5.0.0.1)
>> user = User.first
  User Load (0.4ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-08-26 04:58:19", updated_at: "2017-08-26 04:58:19", password_digest: "$2a$10$0FM4wRDzxU.nOpDa5RmxZuyTz/omIcX4Qm4nguWAde0...", remember_digest: nil, admin: true, activation_digest: "$2a$10$gQQiCXRCh4ZDsruNiSv5euZW4Vwt7kcKm6tZkxM1MMg...", activated: true, activated_at: "2017-08-26 04:58:19", reset_digest: nil, reset_sent_at: nil>
>> 
?> user.followers.count
   (0.5ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 38

演習2

先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。

49人のユーザーをフォローしていることを確認。

?> user.following.count
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 49

14.2.2 統計と [Follow] フォーム

本章での学び

【routes】Usersコントローラにアクションを追加する

Usersコントローラに、followingアクションと、followerアクションを追加する。 memberメソッドを使うことで、メンバールーティングとして追加できる。 /users/1/following や、/users/2/followers/ というURLをGETで送信する。

  resources :users do
    member do
      get :following,:followers
    end
  end

ルーティングの結果を確認する。 following_user_pathと、follower_users_pathが使えるようになっていることを確認。

yokoyan:~/workspace/sample_app (following-users) $ rails routes
                 Prefix Verb   URI Pattern                             Controller#Action
                   root GET    /                                       static_pages#home
                   help GET    /help(.:format)                         static_pages#help
                  about GET    /about(.:format)                        static_pages#about
                contact GET    /contact(.:format)                      static_pages#contact
                 signup GET    /signup(.:format)                       users#new
                  login GET    /login(.:format)                        sessions#new
                        POST   /signup(.:format)                       users#create
                        POST   /login(.:format)                        sessions#create
                 logout DELETE /logout(.:format)                       sessions#destroy
         following_user GET    /users/:id/following(.:format)          users#following
         followers_user GET    /users/:id/followers(.:format)          users#followers

【view】フォロワーの統計情報を表示するパーシャル

パーシャルを新規作成する。

yokoyan:~/workspace/sample_app (following-users) $ touch app/views/shared/_stats.html.erb

中身を実装する。

<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count%>
    </strong>
    followers
  </a>
</div>

【view】統計情報パーシャルの呼び出し

作成した統計情報パーシャルを呼び出す。

  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="stats">
        <%= render 'shared/stats' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>

【scss】Homeページのサイドバー用のSCSS

本章で使うSCSSを作成する。

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float:left;
    padding: 0 10px;
    border-left:1px solid $gray-lighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}

.users_follow {
  padding: 0;
}

【view】フォロー・フォロー解除のパーシャル

パーシャルを新規作成する。

yokoyan:~/workspace/sample_app (following-users) $ touch app/views/users/_follow_form.html.erb

パーシャルの中身を実装する。

<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

【routes】Relationshipリソース用のルーティングを追加する

フォロー(create)と、フォロー解除(destroy)を追加する。

  resources :relationships, only: [:create, :destroy]

追加したルーティングを確認する。

yokoyan:~/workspace/sample_app (following-users) $ rails routes
                 Prefix Verb   URI Pattern                             Controller#Action
・・・略・・・
          relationships POST   /relationships(.:format)                relationships#create
           relationship DELETE /relationships/:id(.:format)            relationships#destroy

【view】フォロー・フォロー解除のフォームパーシャルを作成する

パーシャルを新規作成する。

yokoyan:~/workspace/sample_app (following-users) $ touch app/views/users/_follow.html.erb
yokoyan:~/workspace/sample_app (following-users) $ touch app/views/users/_unfollow.html.erb

フォロー時のフォームパーシャルを実装する。 フォロー登録を行うために、postメソッドを送信する。 フォローするユーザのfollowed_idはhidden属性で生成し、submit時にコントローラへ送信する。

<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary"%>
<% end %>

フォロー解除時のフォームパーシャルを実装する。 フォロー解除を行うために、deleteメソッドを送信する。

<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
            html: { method: :delete}) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

【view】プロフィールページにパーシャルを追加する。

各ユーザのプロフィールページに、フォロー数・フォロワー数と、フォローボタンを追加する。

<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="col-md-8">
    <%= render 'follow_form' if logged_in? %>

演習1

ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では [Unfollow] ボタンが表示されているはずです。さて、/users/1 にアクセスすると、どのような結果が表示されるでしょうか?

/users/2にアクセス。 Followボタンが表示される。

image

/users/5にアクセス。 Unfollowボタンが表示される。

image

/users/1にアクセス。 どちらのボタンも表示されない。(ログインユーザ自身であるため)

image

演習2

ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。

Homeページを表示。 フォロー数、フォロワー数の統計情報が表示されている。

image

プロフィールページを表示。 フォロー数、フォロワー数の統計情報が表示されている。

image

演習3

Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28に示したテストを追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。

Homeページにフォロー数、フォロワー数が表示されているかテストする。

  test "count relationships" do
    log_in_as(@user)
    get root_path
    assert_match @user.active_relationships.count.to_s, response.body
    assert_match @user.passive_relationships.count.to_s, response.body
  end

プロフィールページにフォロー数、フォロワー数が表示されているかテストする。

  test "profile display" do
    get user_path(@user)
    assert_template 'users/show'
    assert_select 'title', full_title(@user.name)
    assert_select 'h1', text: @user.name
    assert_select 'h1>img.gravator'
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination'
    @user.microposts.paginate(page: 1) do |micropost|
      assert_match micropost.content, response.body
    end
    assert_select 'div.pagination', count:1
    assert_match @user.active_relationships.count.to_s, response.body
    assert_match @user.passive_relationships.count.to_s, response.body
  end

テストがgreenになることを確認。

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 12301
Started with run options --seed 59991
DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                   ] 35% Time: 00:00:03,  ETA: 00:00:06
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as
Examples:
get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  64/64: [==================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.64197s
64 tests, 277 assertions, 0 failures, 0 errors, 0 skips

14.2.3 [Following] と [Followers] ページ

本章での学び

【test】フォロー・フォロワーページの認可をテストする

どちらのページも、ログインを必須とする。

  • should redirect following when not logged in

    • getリクエスト送信(following_user_path(@user))
    • ログインページヘリダイレクトされること
  • should redirect followers when not logged in

    • getリクエスト送信(followers_user_path(@user))
    • ログインページヘリダイレクトされること

上記を踏まえて実装する。

  test "should redirect following when not logged in" do
    get following_user_path(@user)
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    get follower_users_path(@user)
    assert_redirected_to login_url
  end

【controller】followingアクションとfollowersアクションの追加

Usersコントローラに2つのアクションを追加する。 それぞれのアクションで、タイトル設定、ユーザ検索、 フォロー(またはフォロワー)ユーザのページ処理、show_followビューの呼び出しを行う。

また、どちらのアクションもログインが必須であるため、before_actionに追加する。

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
・・・略・・・
  def following
    @title = "Following"
    @user = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end
  
  def followers
    @title = "Followers"
    @user = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

【view】フォロー(またはフォロワー)ユーザを表示する

コントローラから呼び出される新規ビューを作成する。

yokoyan:~/workspace/sample_app (following-users) $ touch app/views/users/show_follow.html.erb

ビューを実装する。

<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats'%>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size:30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

【test】動作確認

現時点でテストがgreenになることを確認。

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 14693
Started with run options --seed 46484

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                   ] 62% Time: 00:00:03,  ETA: 00:00:02
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  66/66: [==================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.25675s
66 tests, 279 assertions, 0 failures, 0 errors, 0 skips

【test】統合テストの作成

HTML構造を網羅的にチェックするテストは壊れやすいため、基本的なテストに留める。

統合テストを生成する。

yokoyan:~/workspace/sample_app (following-users) $ rails generate integration_test following
Running via Spring preloader in process 14844
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  test_unit
      create    test/integration/following_test.rb

【fixture】テストデータの作成

フォローとフォロワーのデータを生成する。 ユーザの関連付けは、idでも行うことができる。

one:
  follower: michael
  followed: lana

two:
  follower: michael
  followed: malory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

【test】統合テストの実装

  • setup

    • fixtureからmichaelを取得
    • michaelでログインする
  • following page

    • getリクエストを送信(following_user_path(@user))
    • フォローユーザが空ではないことを確認
    • フォローユーザの数がHTML内に存在すること
    • フォローユーザの数だけ繰り返す
      • フォローユーザのプロフィールページへのリンクが存在すること
  • followers page

    • getリクエストを送信(followers_user_path(@user))
    • フォロワーユーザが空ではないことを確認
    • フォロワーユーザの数がHTML内に存在すること
    • フォロワーユーザの数だけ繰り返す
      • フォロワーユーザのプロフィールページへのリンクが存在すること

上記を踏まえて実装する。

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followersh page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

テストの実施

テストがgreenになることを確認。

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 15264
Started with run options --seed 61814

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                   ] 16% Time: 00:00:01,  ETA: 00:00:10
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  68/68: [==================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.08686s
68 tests, 289 assertions, 0 failures, 0 errors, 0 skips

演習1

ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?

/users/1/followersへアクセス。

image

サイドバーの画像がリンクとして機能していることを確認。

image

/users/1/followingへアクセス。

image

演習2

リスト 14.29のassert_selectに関連するコードをコメントアウトしてみて、テストが正しく red に変わることを確認してみましょう。

該当箇所をコメントアウトする。

          <% @users.each do |user| %>
            <%= #link_to gravatar_for(user, size:30), user %>
          <% end %>

テスト結果がredになることを確認。 (動作確認後は戻しておくこと)

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 16122
Started with run options --seed 59647

ERROR["test_followersh_page", FollowingTest, 1.2620783941820264]
 test_followersh_page#FollowingTest (1.26s)
SyntaxError:         SyntaxError: /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:19: unknown regexp options - ct
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:20: syntax error, unexpected '<'
          </aside>
           ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:22: unknown regexp option - h
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:22: syntax error, unexpected tINTEGER, expecting ')'
        ...output_buffer.safe_append='</h3>
        ...                               ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:24: syntax error, unexpected keyword_class, expecting keyword_do or '{' or '('
        @output_buffer.safe_append='      <ul class="users follow">
                                                   ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:26: syntax error, unexpected '<', expecting ')'
              </ul>
               ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:29: unknown regexp options - dv
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:30: syntax error, unexpected '<'
        </div>'.freeze;@output_buffer.to_s
         ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:30: unterminated regexp meets end of file
            app/controllers/users_controller.rb:69:in `followers'
            test/integration/following_test.rb:23:in `block in <class:FollowingTest>'

ERROR["test_following_page", FollowingTest, 1.299861785955727]
 test_following_page#FollowingTest (1.30s)
SyntaxError:         SyntaxError: /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:19: unknown regexp options - ct
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:20: syntax error, unexpected '<'
          </aside>
           ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:22: unknown regexp option - h
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:22: syntax error, unexpected tINTEGER, expecting ')'
        ...output_buffer.safe_append='</h3>
        ...                               ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:24: syntax error, unexpected keyword_class, expecting keyword_do or '{' or '('
        @output_buffer.safe_append='      <ul class="users follow">
                                                   ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:26: syntax error, unexpected '<', expecting ')'
              </ul>
               ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:29: unknown regexp options - dv
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:30: syntax error, unexpected '<'
        </div>'.freeze;@output_buffer.to_s
         ^
        /home/ubuntu/workspace/sample_app/app/views/users/show_follow.html.erb:30: unterminated regexp meets end of file
            app/controllers/users_controller.rb:62:in `following'
            test/integration/following_test.rb:14:in `block in <class:FollowingTest>'

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                   ] 30% Time: 00:00:03,  ETA: 00:00:07
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  68/68: [==================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.38619s
68 tests, 279 assertions, 0 failures, 2 errors, 0 skips

14.2.4 [Follow] ボタン (基本編)

本章での学び

【controller】Relationshipsコントローラの作成

コマンドで自動生成する。

yokoyan:~/workspace/sample_app (following-users) $ rails generate controller Relationships
Running via Spring preloader in process 1237
Expected string default value for '--jbuilder'; got true (boolean)
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--assets'; got true (boolean)
      create  app/controllers/relationships_controller.rb
      invoke  erb
      create    app/views/relationships
      invoke  test_unit
      create    test/controllers/relationships_controller_test.rb
      invoke  helper
      create    app/helpers/relationships_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/relationships.coffee
      invoke    scss
      create      app/assets/stylesheets/relationships.scss

【test】Relationshipsコントローラのテストを実装

  • create should require logged-in user
    • Relationship.countが変わらないこと
      • postリクエストを送信(relationships_path)
    • ログイン画面へリダイレクトされること
  • destroy should require logged-in user
    • Relationship.countが変わらないこと
      • deleteリクエストを送信(relationship_path(relationship(:one)))
        • fixtureのrelathionship内の:oneを削除
    • ログイン画面へリダイレクトされること

上記を踏まえて実装する。

  test "create should require logged-in user" do
    assert_no_difference "Relationship.count" do
      post relationships_path
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference "Relationship.count" do
      delete relationship_path(relationships(:one))
    end
    assert_redirected_to login_url
  end

【controller】リレーションシップのアクセス制御

テストコードをgreenにするために、Relationshipsコントローラに、before_actionを追加する。また、createアクションとdestroyアクションを追加する。

class RelationshipsController < ApplicationController
  before_action  :logged_in_user
  
  def create
  end
  
  def destroy
  end
end

【controller】アクションの実装

createアクション、destroyアクションの中身を実装する。

  def create
    user = User.find(params[:followed_id])
    current_user.follow(user)
    redirect_to user
  end

  def destroy
    user = Relationship.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end

演習1

ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?

/users/2 を開く。

image

Followを実行。

image

Unfollowを実行。

image

演習2

先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?

フォロー時のログは以下の通り。 Rendering users/show.html.erbが描画されている。

Started POST "/relationships" for 222.229.53.237 at 2017-08-27 12:31:59 +0000
Cannot render console from 222.229.53.237! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by RelationshipsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"a9o2UgH0P0jYefj9EJk/xgQ3vBc54IOBKuqO/OCWIYH6yKHPGhW7I2lDLzcmdTgxwgH7pq4sfEZXbm5RweEJIA==", "followed_id"=>"2", "commit"=>"Follow"}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  SQL (1.8ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", 2017-08-27 12:31:59 UTC], ["updated_at", 2017-08-27 12:31:59 UTC]]
   (10.6ms)  commit transaction
Redirected to https://rails-tutorial-yokoyan.c9users.io/users/2
Completed 302 Found in 75ms (ActiveRecord: 15.2ms)


Started GET "/users/2" for 222.229.53.237 at 2017-08-27 12:32:00 +0000
Cannot render console from 222.229.53.237! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"2"}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendering users/show.html.erb within layouts/application
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 2]]
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 2]]
  Rendered shared/_stats.html.erb (9.8ms)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 1], ["id", 2], ["LIMIT", 1]]
  Relationship Load (0.4ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
  Rendered users/_unfollow.html.erb (5.1ms)
  Rendered users/_follow_form.html.erb (11.7ms)
  Micropost Exists (0.2ms)  SELECT  1 AS one FROM "microposts" WHERE "microposts"."user_id" = ? LIMIT ?  [["user_id", 2], ["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 2]]
  Micropost Load (0.4ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? OFFSET ?  [["user_id", 2], ["LIMIT", 30], ["OFFSET", 0]]
  Rendered collection of microposts/_micropost.html.erb [30 times] (17.4ms)
  CACHE (0.0ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 2]]
  Rendered users/show.html.erb within layouts/application (62.4ms)
  Rendered layouts/_rails_default.html.erb (71.1ms)
  Rendered layouts/_shim.html.erb (0.5ms)
  Rendered layouts/_header.html.erb (1.3ms)
  Rendered layouts/_footer.html.erb (0.6ms)
Completed 200 OK in 160ms (Views: 141.1ms | ActiveRecord: 3.4ms)

フォロー解除時のログは以下の通り。 Rendering users/show.html.erbが描画されている。

Started DELETE "/relationships/89" for 222.229.53.237 at 2017-08-27 12:35:21 +0000
Cannot render console from 222.229.53.237! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by RelationshipsController#destroy as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"yORU0Rlu5i+Xb+U+5b40amqm6iCIj7qOZEbZhIKfkOlZ9sNMAo9iRCZVMvTTUjOdrJCtkR9DRUkZwjkpo+i4SA==", "commit"=>"Unfollow", "id"=>"89"}
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Relationship Load (0.3ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."id" = ? LIMIT ?  [["id", 89], ["LIMIT", 1]]
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Relationship Load (0.4ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
   (0.2ms)  begin transaction
  SQL (36.1ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 89]]
   (23.0ms)  commit transaction
Redirected to https://rails-tutorial-yokoyan.c9users.io/users/2
Completed 302 Found in 75ms (ActiveRecord: 60.6ms)


Started GET "/users/2" for 222.229.53.237 at 2017-08-27 12:35:21 +0000
Cannot render console from 222.229.53.237! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"2"}
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendering users/show.html.erb within layouts/application
   (0.4ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 2]]
   (0.4ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 2]]
  Rendered shared/_stats.html.erb (10.6ms)
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Exists (0.4ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 1], ["id", 2], ["LIMIT", 1]]
  Rendered users/_follow.html.erb (4.5ms)
  Rendered users/_follow_form.html.erb (11.1ms)
  Micropost Exists (0.4ms)  SELECT  1 AS one FROM "microposts" WHERE "microposts"."user_id" = ? LIMIT ?  [["user_id", 2], ["LIMIT", 1]]
   (0.3ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 2]]
  Micropost Load (0.9ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? OFFSET ?  [["user_id", 2], ["LIMIT", 30], ["OFFSET", 0]]
  Rendered collection of microposts/_micropost.html.erb [30 times] (22.3ms)
  CACHE (0.0ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 2]]
  Rendered users/show.html.erb within layouts/application (107.3ms)
  Rendered layouts/_rails_default.html.erb (73.7ms)
  Rendered layouts/_shim.html.erb (0.4ms)
  Rendered layouts/_header.html.erb (1.5ms)
  Rendered layouts/_footer.html.erb (0.7ms)
Completed 200 OK in 202ms (Views: 190.4ms | ActiveRecord: 3.6ms)

14.2.5 [Follow] ボタン (Ajax編)

本章での学び

フォロー・フォロー解除後に元のプロフィールページにリダイレクトしているが、 Ajaxを使ってリファクタリングを行う。

【view】Ajaxを使ったフォーム

form_for ・・・, remote:trureでAjaxが使えるようになる。 Ajaxを使ったフォローフォーム。

<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary"%>
<% end %>

Ajaxを使ったフォロー解除フォーム。

<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
            html: { method: :delete},
            remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

erbによって、生成されるHTMLは以下の通り。 「JavaScriptは全面に出すべからず」という哲学により、data-remote="true"という属性が追加されるだけ。

<form class="new_relationship" id="new_relationship" action="/relationships" accept-charset="UTF-8" data-remote="true" method="post">

【controller】RelationshipsコントローラでAjaxリクエストに対応する

コントローラでAjaxリクエストを受け取るために、respond_toメソッドを追加する。

クライアントからの要求に応じてフォーマットを変更する。

format.出力形式で返すフォーマットを定義する。 1つのアクションから複数のフォーマットで返す場合、複数行記述する。 ただし、実行されるのはそのうち1つになる。

つまり、フォーマットがhtmlなら、ユーザプロフィールへリダイレクトして、 フォーマットがjsなら、Ajax処理を行う。

※ビューで変数を扱うために、user@userに変更する。

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

【config】jsが無効になっていた時の設定

フォームで:remote => trueを使用した場合のauthenticity_tokenのデフォルトの動作を設定する。

  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # 認証トークンをremoteフォームに埋め込む
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end

【view】JavaScriptと埋め込みRubyを使ってフォローの関係性を作成・削除する

Ajaxリクエストを送信した際に、Railsは自動的にアクションと同じ名前を持つjs.erbファイルを呼び出す。(create.js.erbや、destroy.js.erbなど)

js.erbファイルを新規に作成する。

yokoyan:~/workspace/sample_app (following-users) $ touch app/views/relationships/create.js.erb
yokoyan:~/workspace/sample_app (following-users) $ touch app/views/relationships/destroy.js.erb

以下の仕様で中身を実装する。

  • id=follow_formのHTLM要素を、render('users/unfollow')で更新する。
  • id=followersのHTML要素を、@user.followers.countの結果で更新する。
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');

以下の仕様で中身を実装する。

  • id=follow_formのHTML要素を、'render(‘users/follow’)‘で更新する。
  • id=followersのHTML要素を、'@user.followers.count'の結果で更新する。
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');

演習1

ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。

/users/2 にアクセスする。

image

フォローする。

image

フォロー解除する。

image

演習2

先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。

フォローした時。 Processing by RelationshipsController#create as JSが動いている。

Started POST "/relationships" for 222.229.53.237 at 2017-08-27 22:04:17 +0000
Cannot render console from 222.229.53.237! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by RelationshipsController#create as JS
  Parameters: {"utf8"=>"✓", "followed_id"=>"2", "commit"=>"Follow"}
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  SQL (0.4ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", 2017-08-27 22:04:17 UTC], ["updated_at", 2017-08-27 22:04:17 UTC]]
   (14.1ms)  commit transaction
  Rendering relationships/create.js.erb
  Relationship Load (0.3ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
  Rendered users/_unfollow.html.erb (3.0ms)
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 2]]
  Rendered relationships/create.js.erb (7.1ms)
Completed 200 OK in 39ms (Views: 9.9ms | ActiveRecord: 15.9ms)

フォロー解除した時。 Processing by RelationshipsController#destroy as JSが動いている。

Started DELETE "/relationships/90" for 222.229.53.237 at 2017-08-27 22:06:08 +0000
Cannot render console from 222.229.53.237! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by RelationshipsController#destroy as JS
  Parameters: {"utf8"=>"✓", "commit"=>"Unfollow", "id"=>"90"}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Relationship Load (0.1ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."id" = ? LIMIT ?  [["id", 90], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Relationship Load (0.2ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (0.4ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 90]]
   (18.4ms)  commit transaction
  Rendering relationships/destroy.js.erb
  Rendered users/_follow.html.erb (2.3ms)
   (0.4ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 2]]
  Rendered relationships/destroy.js.erb (6.8ms)
Completed 200 OK in 66ms (Views: 9.4ms | ActiveRecord: 20.6ms)

14.2.6 フォローをテストする

本章での学び

通常のリクエスト版とAjax版のテストコードを作成する。

【test】フォロー・フォロー解除ボタンをテストする

Ajaxのテストでは、xhr :trueオプションを使用する。 XmlHttpRequestの頭文字。

  • should follow a user the standard way

    • @user.following.countが1増えないこと
      • postリクエストを送信(relationship_path)
        • params{ followed_id: @other.id}
  • should follow a user with Ajax

    • @user.following.countが1増えないこと
      • Ajaxでpostリクエストを送信(relationship_path, xhr: true
        • params{ followed_id: @other.id}
  • should unfollow a user the standard way

    • @userが@otherをフォローする
    • @user.active_relationshipsから、@other.idを検索する
    • @user.following.countが-1されないこと
      • deleteリクエストを送信(relationship_path)
  • should unfollow a user with Ajax

    • @userが@otherをフォローする
    • @user.active_relationshipsから、@other.idを検索する
    • @user.following.countが-1されないこと
      • Ajaxでdeleteリクエストを送信(relationship_path)

上記を踏まえて実装する。

  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

テスト実施

テスト結果がgreenになることを確認

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 1957
Started with run options --seed 24789

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  74/74: [====================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.83747s
74 tests, 297 assertions, 0 failures, 0 errors, 0 skips

演習1

リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?

フォロー1 通常tリクエストをコメントアウト。

    respond_to do |format|
      # format.html { redirect_to @user }
      format.js
    end

post relationships_path, params: { followed_id: @other.id }で落ちる。

ERROR["test_should_follow_a_user_the_standard_way", FollowingTest, 4.909926524851471]
 test_should_follow_a_user_the_standard_way#FollowingTest (4.91s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:8:in `create'
            test/integration/following_test.rb:34:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:33:in `block in <class:FollowingTest>'

フォロー2 Ajaxリクエストをコメントアウト。

    respond_to do |format|
      format.html { redirect_to @user }
      # format.js
    end

この場合はテストはエラーにならず、テストは成功する。

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 1227
Started with run options --seed 12214

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                                  ] 40% Time: 00:00:03,  ETA: 00:00:05
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  74/74: [=================================================================================================================================] 100% Time: 00:00:06, Time: 00:00:06

Finished in 6.70043s

フォロー解除1 通常のリクエストをコメントアウト。

    respond_to do |format|
      # format.html { redirect_to @user }
      format.js
    end

delete relationship_path(relationship)で落ちる。

ERROR["test_should_unfollow_a_user_the_standard_way", FollowingTest, 4.035211271606386]
 test_should_unfollow_a_user_the_standard_way#FollowingTest (4.04s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:18:in `destroy'
            test/integration/following_test.rb:48:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:47:in `block in <class:FollowingTest>'

フォロー解除2 Ajaxリクエストをコメントアウト。

    respond_to do |format|
      format.html { redirect_to @user }
      # format.js
    end

フォロー時と同じく、成功する。 おそらく、Ajax無効時にtrueにする設定が入っているため??

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 1602
Started with run options --seed 3119

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only=================================                 ] 87% Time: 00:00:04,  ETA: 00:00:01
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  74/74: [=================================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.53267s
74 tests, 297 assertions, 0 failures, 0 errors, 0 skips

この状態で、Ajax無効時の設定をコメントアウトして、テストしてみる。

    # 認証トークンをremoteフォームに埋め込む
    # config.action_view.embed_authenticity_token_in_remote_forms = true

それでもテストが通る。関係無かったようだ・・・。

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 1798
Started with run options --seed 55359

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                                   ] 6% Time: 00:00:02,  ETA: 00:00:31
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  74/74: [=================================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.70124s
74 tests, 297 assertions, 0 failures, 0 errors, 0 skips

演習2

リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。

ポスト時のxhr: trueを削除する。

  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 }
      post relationships_path, params: { followed_id: @other.id }
    end
  end

この場合でもエラーは発生しない。

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 2013
Started with run options --seed 52161

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only==========================                        ] 82% Time: 00:00:03,  ETA: 00:00:01
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  74/74: [=================================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.16438s
74 tests, 297 assertions, 0 failures, 0 errors, 0 skips

おわりに

フォロー、フォロー解除機能の実装が完了しました。 RailsだとAjaxの処理も簡単に書けますね。 最後の演習で、ajax部分をコメントアウトしてもテストがgreenになる理由がわかっていません。 もやもやするな・・・。