紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、Ruby on Railsチュートリアルの解答やAmazon Web Servicesについて学んだことをこつこつ情報発信するブログです。

【14章】Ruby on Railsチュートリアル演習まとめ&解答例【14.3 ステータスフィード】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 14章 14.3 ステータスフィードの演習まとめ&解答例です。

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

動作環境

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

14.3.1 動機と計画

本章での学び

【test】ステータスフィードのテスト

  • feed should have the right posts
    • fixtureから、michaelユーザの情報を取得する
    • fixtureから、archerユーザの情報を取得する
    • fixtureから、lanaユーザの情報を取得する
    • フォローしているユーザの投稿を確認
      • lanaのマイクロポストを取得
        • michaelのフィードに含まれていること
    • 自分自身の投稿を確認
      • michaelのマイクロポストを取得
        • michaelのフィードに含まれていること
    • フォローしていないユーザの投稿を確認
      • archerのマイクロポストを取得
        • michaelのフィードに含まれていないこと

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

  test "feed should have the right posts" do
    michael = users(:michael)
    archer  = users(:archer)
    lana    = users(:lana)
    # フォローしているユーザの投稿を確認
    lana.microposts.each do |post_following|
      assert michael.feed.include?(post_following)
    end
    # 自分自身の投稿を確認
    michael.microposts.each do |post_self|
      assert michael.feed.include?(post_self)
    end
    # フォローしていないユーザの投稿を確認
    archer.microposts.each do |post_unfollowed|
      assert_not michael.feed.include?(post_unfollowed)
    end
  end

テストは現時点ではredとなる。

演習1

マイクロポストのidが正しく並んでいると仮定して (すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。ヒント: 13.1.4で実装したdefault_scopeを思い出してください。

user自身とuserがフォローしているマイクロポストが昇順で取得できる。 default_scopeで降順になるように実装していれば、マイクロポストが降順で取得できる。

14.3.2 フィードを初めて実装する

本章での学び

【事前準備】Rubyのmapメソッド

mapメソッドは、&とメソッドに対応するシンボルを使った短縮表記ができる。

okoyan:~/workspace/sample_app (following-users) $ rails console
Running via Spring preloader in process 2367
Loading development environment (Rails 5.0.0.1)
>> [1,2,3,4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]
>> 
?> [1,2,3,4].map(&:to_s)
=> ["1", "2", "3", "4"]
>> 

カンマ区切りの文字列としてつなげることもできる。

?> [1,2,3,4].map(&:to_s).join(', ')
=> "1, 2, 3, 4"

user.followingにある各要素のidを呼び出し、フォローしているユーザのidを配列とする。

?> User.first.following.map(&:id)
  User Load (0.6ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  User Load (0.7ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

ActiveRecordで用意しているfollowing_idsメソッドを使うことで、上記と同じ結果が得られる。

?> User.first.following_ids
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.3ms)  SELECT "users".id FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

応用して、カンマ区切りの文字列を取得することもできる。

?> User.first.following_ids.join(', ')
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.4ms)  SELECT "users".id FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> "3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"

【test】テスト実行

テスト実行時に以下のようなエラーが発生。

7ffe7035b000-7ffe7035d000 r--p 00000000 00:00 0                          [vvar]
7ffe7035d000-7ffe7035f000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]


[NOTE]
You may have encountered a bug in the Ruby interpreter or extension libraries.
Bug reports are welcome.
For details: http://www.ruby-lang.org/bugreport.html

エラーになるため、bundle updateを実行。

yokoyan:~/workspace/sample_app (following-users) $ bundle update
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/.........
Fetching version metadata from https://rubygems.org/..
Fetching dependency metadata from https://rubygems.org/.
Resolving dependencies.....
Using rake 12.0.0
Using CFPropertyList 2.3.5
Using concurrent-ruby 1.0.5
Installing i18n 0.8.6 (was 0.8.1)
Installing minitest 5.10.3 (was 5.10.1)
Using thread_safe 0.3.6
Using builder 3.2.3
Using erubis 2.7.0
Installing mini_portile2 2.2.0 (was 2.1.0)
Installing rack 2.0.3 (was 2.0.1)
Using nio4r 1.2.1
Using websocket-extensions 0.1.2
Using mime-types-data 3.2016.0521
Using arel 7.1.4
Using ansi 1.5.0
Using execjs 2.7.0
Using bcrypt 3.1.11
Installing rb-fsevent 0.10.2 (was 0.9.8)
Using ffi 1.9.18
Using will_paginate 3.1.0
Using bundler 1.14.6
Using byebug 9.0.0
Using coderay 1.1.1
Using coffee-script-source 1.12.2
Using method_source 0.8.2
Installing thor 0.20.0 (was 0.19.4)
Installing debug_inspector 0.0.3 (was 0.0.2) with native extensions
Using excon 0.58.0
Using formatador 0.2.5
Using multi_json 1.12.1
Using ipaddress 0.8.3
Using xml-simple 1.1.5
Using inflecto 0.0.2
Using json 1.8.6
Using trollop 2.1.2
Installing lumberjack 1.0.12 (was 1.0.11)
Using nenv 0.3.0
Using shellany 0.0.1
Using slop 3.6.0
Using guard-compat 1.2.1
Using mini_magick 4.7.0
Using ruby-progressbar 1.8.1
Using puma 3.4.0
Installing tilt 2.0.8 (was 2.0.6)
Using spring 1.7.2
Using sqlite3 1.3.11
Installing turbolinks-source 5.0.3 (was 5.0.0)
Using fission 0.5.0
Using faker 1.6.6
Installing tzinfo 1.2.3 (was 1.2.2)
Installing nokogiri 1.8.0 (was 1.7.0.1) with native extensions
Using rack-test 0.6.3
Using sprockets 3.7.1
Using websocket-driver 0.6.5
Using mime-types 3.1
Installing autoprefixer-rails 7.1.3 (was 6.7.7.1)
Using uglifier 3.0.0
Installing rb-inotify 0.9.10 (was 0.9.8)
Using bootstrap-will_paginate 0.0.10
Using coffee-script 2.4.1
Using fog-core 1.45.0
Using notiffany 0.1.1
Using pry 0.10.4
Using guard-minitest 2.4.4
Using minitest-reporters 1.1.9
Using turbolinks 5.0.1
Using activesupport 5.0.0.1
Using loofah 2.0.3
Using rbvmomi 1.11.3
Installing mail 2.6.6 (was 2.6.4)
Installing sass-listen 4.0.0
Using listen 3.0.8
Using fog-json 1.0.2
Using fog-xml 0.1.3
Using fog-local 0.3.1
Using fog-vmfusion 0.1.0
Installing rails-dom-testing 2.0.3 (was 2.0.2)
Installing globalid 0.4.0 (was 0.3.7)
Using activemodel 5.0.0.1
Using jbuilder 2.4.1
Using rails-html-sanitizer 1.0.3
Installing fog-vsphere 1.12.0 (was 1.11.3)
Installing sass 3.5.1 (was 3.4.23)
Using guard 2.13.0
Using spring-watcher-listen 2.0.0
Using fog-aliyun 0.2.0
Using fog-brightbox 0.13.0
Using fog-dnsimple 1.0.0
Using fog-openstack 0.1.21
Using fog-profitbricks 4.0.0
Using fog-sakuracloud 1.7.5
Using fog-serverlove 0.1.2
Using fog-softlayer 1.1.4
Using fog-storm_on_demand 0.1.1
Using fog-atmos 0.1.0
Installing fog-aws 1.4.1 (was 1.4.0)
Using fog-cloudatcost 0.1.2
Using fog-digitalocean 0.3.0
Using fog-dynect 0.0.3
Using fog-ecloud 0.3.0
Using fog-google 0.1.0
Using fog-powerdns 0.1.1
Using fog-rackspace 0.1.5
Using fog-radosgw 0.0.5
Using fog-riakcs 0.1.0
Using fog-terremark 0.1.0
Using fog-voxel 0.1.0
Using fog-xenserver 0.3.0
Using activejob 5.0.0.1
Using activerecord 5.0.0.1
Using carrierwave 1.1.0
Using actionview 5.0.0.1
Using bootstrap-sass 3.3.6
Using fog 1.40.0
Using actionpack 5.0.0.1
Using actioncable 5.0.0.1
Using actionmailer 5.0.0.1
Using railties 5.0.0.1
Using sprockets-rails 3.2.0
Using rails-controller-testing 0.1.1
Using coffee-rails 4.2.1
Using jquery-rails 4.1.1
Using web-console 3.1.1
Using rails 5.0.0.1
Using sass-rails 5.0.6
Bundle updated!
Gems in the group production were not installed.

【test】再度テストを実行

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

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

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                                                                                                     ] 18% Time: 00:00:02,  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)
  75/75: [====================================================================================================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.33761s
75 tests, 335 assertions, 0 failures, 0 errors, 0 skips

演習1

リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

user.feedの実行結果は以下の通り。 フォローしているユーザのIDと、自分のIDを渡している。

yokoyan:~/workspace/sample_app (following-users) $ rails console
Running via Spring preloader in process 1248
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.feed
   (0.6ms)  SELECT "users".id FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
  Micropost Load (3.2ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id IN (3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51) OR user_id = 1) ORDER BY "microposts"."created_at" DESC

つまり、自分の投稿を含めないようにするには、2つ目の引数を削除する。

  def feed
    # Micropost.where("user_id = ?", id)
    # Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    Micropost.where("user_id IN (?) ", following_ids)
  end

以下のテストで結果がredとなる。

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 1.9879938876256347]
 test_micropost_interface#MicropostsInterfaceTest (1.99s)
        Expected at least 1 element matching "div.pagination", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:14:in `block in <class:MicropostsInterfaceTest>'

 FAIL["test_feed_should_have_the_right_posts", UserTest, 3.517132450826466]
 test_feed_should_have_the_right_posts#UserTest (3.52s)
        Expected false to be truthy.
        test/models/user_test.rb:108:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:107:in `block in <class:UserTest>'

演習2

リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

演習1と逆で、1つめの引数を削除する。 (試作フィードの状態に戻すことになる)

  def feed
    Micropost.where("user_id = ?", id)
    # Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end

以下のテストがredになることを確認。

 FAIL["test_feed_should_have_the_right_posts", UserTest, 2.452872692141682]
 test_feed_should_have_the_right_posts#UserTest (2.45s)
        Expected false to be truthy.
        test/models/user_test.rb:104:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:103:in `block in <class:UserTest>'

演習3

リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。

フォローしていないユーザーの投稿を含めるためには、 micropostsテーブルのすべての情報を取得する。

  def feed
    # Micropost.where("user_id = ?", id)
    # Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    Micropost.all
  end

以下のテストがredになることを確認。 フォローしていないユーザが含まれないことが期待値であるためエラーとなっている。

 FAIL["test_feed_should_have_the_right_posts", UserTest, 1.5933369509875774]
 test_feed_should_have_the_right_posts#UserTest (1.59s)
        Expected true to be nil or false
        test/models/user_test.rb:112:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:111:in `block in <class:UserTest>'

14.3.3 サブセレクト

本章での学び

今の実装ではパフォーマンスに懸念があるため、サブセレクトを使って解決する。

【model】feedメソッドのリファクタリング

同じ変数を複数の場所に挿入する場合は、whereメソッド内の変数にキーと値のペアにしたほうが便利とのこと。

  def feed
    # 自分のみ
    # Micropost.where("user_id = ?", id)
    # フォローしているユーザか自分の投稿
    # Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id)
    # フォローしていないユーザも含める
    # Micropost.all
  end

上記からさらに、サブクエリのSQLを追加する。

 def feed
    # フォローしているユーザか自分の投稿
    # Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    # Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id)
    #最終的な実装
    following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id)

動作確認

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

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

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only=====                                             ] 66% 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)
  75/75: [=================================================================================================================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.46490s
75 tests, 335 assertions, 0 failures, 0 errors, 0 skips

ブラウザからもフォローしているユーザのフィードが表示されていることを確認。

image

MASTERブランチへのマージ

yokoyan:~/workspace/sample_app (master) $ rails test
yokoyan:~/workspace/sample_app (master) $ git add -A
yokoyan:~/workspace/sample_app (master) $ git status
yokoyan:~/workspace/sample_app (master) $ git commit -m "Add user following"
yokoyan:~/workspace/sample_app (master) $ git checkout master
yokoyan:~/workspace/sample_app (master) $ git merge following-users 

Herokuへのデプロイ

yokoyan:~/workspace/sample_app (master) $ git push
yokoyan:~/workspace/sample_app (master) $ git push heroku

HerokuのDBのマイグレーション

本番環境のDBのリセット、マイグレーション、テストデータの登録を行う。

yokoyan:~/workspace/sample_app (master) $ heroku pg:reset DATABASE                                                                                                              
yokoyan:~/workspace/sample_app (master) $ heroku run rails db:migrate
yokoyan:~/workspace/sample_app (master) $ heroku run rails db:seed

演習1

Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。

  • feed on Home page
    • getリクエストを送信する(root_path)
    • @userのフィードを生成し、1ページ目を取得する。micropostの数だけ繰り返す。
      • micropost内のcontentをエスケープして、HTML内に表示されること

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

  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(micropost.content), response.body
    end
  end

テスト結果がgreenであることを確認。

yokoyan:~/workspace/sample_app (master) $ rails test
Running via Spring preloader in process 1866
Started with run options --seed 52063

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                    ] 68% 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)
  76/76: [===================================================================================================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.53767s
76 tests, 395 assertions, 0 failures, 0 errors, 0 skips

演習2

リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。

content属性に含まれる改行コードを含め、エスケープするため。

エスケープ処理を外して検証。

  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      # assert_match CGI.escapeHTML(micropost.content), response.body
      assert_match micropost.content, response.body
    end
  end

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

yokoyan:~/workspace/sample_app (master) $ rails test
Running via Spring preloader in process 2044
Started with run options --seed 8292

 FAIL["test_feed_on_Home_page", FollowingTest, 2.3300648159347475]
 test_feed_on_Home_page#FollowingTest (2.33s)
        Expected /I'm\ sorry\.\ Your\ words\ made\ sense,\ but\ your\ sarcastic\ tone\ did\ not\./ to match "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Ruby on Rails Tutorial Sample App</title>\n        \n    <link rel=\"stylesheet\" media=\"all\" href=\"/assets/application-10fa38882e9521d0e9f81abaad450c57aa1940c84d7a510d6d14e6ade7210745.css\" data-turbolinks-track=\"reload\" />\n    <script src=\"/assets/application-8145d5d9dada575ad93c1a2586a1fff566ccef067d7d39e629336a3561895941.js\" data-turbolinks-track=\"reload\"></script>\n        <!--[if lt IE 9]>\n      <script scr=\"//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js\">\n      </script>\n    <![endif]-->\n  </head>\n  <body>\n        <header class=\"navbar navbar-fixed-top navbar-inverse\">\n      <div class=\"container\">\n        <a id=\"logo\" href=\"/\">sample app</a>\n        <nav>\n          <ul class=\"nav navbar-nav navbar-right\">\n            <li><a href=\"/\">Home</a></li>\n            <li><a href=\"/help\">Help</a></li>\n              <li><a href=\"/users\">Users</a></li>\n              <li class=\"dropdown\">\n              <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\">\n                Account <b class=\"caret\"></b>\n              </a>\n              <ul class=\"dropdown-menu\">\n                <li><a href=\"/users/762146111\">Profile</a></li>\n                <li><a href=\"/users/762146111/edit\">Settings</a></li>\n                <li class=\"divider\"></li>\n                <li>\n                  <a rel=\"nofollow\" data-method=\"delete\" href=\"/logout\">Log out</a>\n                </li>\n              </ul>\n            </li>\n          </ul>\n        </nav>\n      </div>\n    </header>\n\n    <div class=\"container\">\n          <div class=\"row\">\n    <aside class=\"col-md-4\">\n      <section class=\"user_info\">\n        <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n<h1>Michael Example</h1>\n<span><a href=\"/users/762146111\">view my profile</a></span>\n<span>34 microposts</span>\n      </section>\n      <section class=\"stats\">\n        <div class=\"stats\">\n  <a href=\"/users/762146111/following\">\n    <strong id=\"following\" class=\"stat\">\n      2\n    </strong>\n    following\n  </a>\n  <a href=\"/users/762146111/followers\">\n    <strong id=\"followers\" class=\"stat\">\n      2\n    </strong>\n    followers\n  </a>\n</div>\n      </section>\n      <section>\n        <form class=\"new_micropost\" id=\"new_micropost\" enctype=\"multipart/form-data\" action=\"/microposts\" accept-charset=\"UTF-8\" method=\"post\"><input name=\"utf8\" type=\"hidden\" value=\"&#x2713;\" />\n  \n  <div class=\"field\">\n    <textarea placeholder=\"Compose new micropost...\" name=\"micropost[content]\" id=\"micropost_content\">\n</textarea>\n  </div>\n  <input type=\"submit\" name=\"commit\" value=\"Post\" class=\"btn btn-primary\" data-disable-with=\"Post\" />\n  <span class=\"picture\">\n    <input accept=\"image/jpeg,image/gif,image/png\" type=\"file\" name=\"micropost[picture]\" id=\"micropost_picture\" />\n  </span>\n</form>\n<script type=\"text/javascript\">\n  $('#micropost_picture').bind('change', function() {\n    var size_in_megabytes = this.files[0].size/1024/1024;\n    if (size_in_megabytes > 5) {\n      alert('Maximum file size is 5MB. Please choose a smaller file.')\n    }\n  });\n</script>\n      </section>\n    </aside>\n    <div class=\"col-md-8\">\n      <h3>Micropost Feed</h3>\n        <ol class=\"microposts\">\n    <li id=\"micropost-941832919\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Writing a short test\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted less than a minute ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/941832919\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-3454773\">\n  <a href=\"/users/409608538\"><img alt=\"Lana Kane\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/de9a58df9617af487e8b28dbb3aa50de?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/409608538\">Lana Kane</a></span>\n  <span class=\"content\">\n    I&#39;m sorry. Your words made sense, but your sarcastic tone did not.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted 10 minutes ago.\n  </span>\n</li>\n<li id=\"micropost-499495288\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    I just ate an orange!\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted 10 minutes ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/499495288\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-12348100\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Sad cats are sad: http://youtu.be/PKffm2uI4dk\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 2 hours ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/12348100\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-970054474\">\n  <a href=\"/users/409608538\"><img alt=\"Lana Kane\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/de9a58df9617af487e8b28dbb3aa50de?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/409608538\">Lana Kane</a></span>\n  <span class=\"content\">\n    Dude, this van&#39;s, like, rolling probable cause.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 4 hours ago.\n  </span>\n</li>\n<li id=\"micropost-19959062\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Sunt ad blanditiis totam aut qui repudiandae rerum id sint.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/19959062\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-58620899\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Quidem doloremque qui consectetur architecto.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/58620899\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-68403196\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Explicabo aliquid voluptatem occaecati consectetur doloremque quia error pariatur id.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/68403196\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-71534927\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Velit error et aut non laboriosam dolorem aut.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/71534927\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-106776847\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Facilis quas et ut minima.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/106776847\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-177734013\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Voluptas consequatur enim aperiam qui.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/177734013\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-228979669\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Enim porro quam magni aliquid at dolores.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/228979669\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-234210660\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Consequatur tenetur mollitia dolores et.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/234210660\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-294621321\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Esse accusantium maxime dolore aut suscipit doloremque soluta ut at.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/294621321\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-328290505\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Omnis eius minima est distinctio voluptatem dolor impedit eum.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/328290505\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-352097504\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Quia et consectetur similique natus.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/352097504\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-406445230\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Et aut veniam aut similique consequatur qui est.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/406445230\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-444017243\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Quod sed est atque non dicta qui.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/444017243\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-488304196\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Aut consequuntur et et molestiae.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/488304196\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-525605047\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Ullam et voluptatem velit enim rerum.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/525605047\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-603694155\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Temporibus at omnis rerum voluptatem consectetur est culpa.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/603694155\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-613834836\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Reiciendis nam quod voluptates et at sit perspiciatis nihil.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/613834836\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-646488084\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Nobis et sunt mollitia natus saepe tempore.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/646488084\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-676538406\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Et molestiae quia blanditiis reiciendis illum sit aut enim.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/676538406\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-706600661\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Aut aliquam est provident nihil cum quaerat.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/706600661\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-762321612\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Sint dolor voluptatum blanditiis eveniet aut temporibus hic.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/762321612\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-792652861\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Error quaerat animi nulla nostrum iste nam.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/792652861\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-828012954\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Velit iste omnis laudantium voluptatibus.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/828012954\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-856985457\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Fugit dolorem quasi et temporibus omnis alias architecto.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/856985457\">delete</a>\n  </span>\n</li>\n<li id=\"micropost-860142042\">\n  <a href=\"/users/762146111\"><img alt=\"Michael Example\" class=\"gravator\" src=\"https://secure.gravatar.com/avatar/03ea78c0884c9ac0f73e6af7b9649e90?s=50\" /></a>\n  <span class=\"user\"><a href=\"/users/762146111\">Michael Example</a></span>\n  <span class=\"content\">\n    Architecto voluptatem voluptatem dolor sed.\n    \n  </span>\n  <span class=\"timestamp\">\n    Posted about 1 month ago.\n      <a data-confirm=\"You sure?\" rel=\"nofollow\" data-method=\"delete\" href=\"/microposts/860142042\">delete</a>\n  </span>\n</li>\n\n  </ol>\n    <div class=\"pagination\"><ul class=\"pagination\"><li class=\"prev previous_page disabled\"><a href=\"#\">&#8592; Previous</a></li> <li class=\"active\"><a rel=\"start\" href=\"/?page=1\">1</a></li> <li><a rel=\"next\" href=\"/?page=2\">2</a></li> <li class=\"next next_page \"><a rel=\"next\" href=\"/?page=2\">Next &#8594;</a></li></ul></div>\n\n    </div>\n  </div>\n\n\n      <footer>\n  <small>\n    The <a href=\"http://railstutorial.jp\">Ruby on Rails Tutorial</a>\n    by <a href=\"http://www.michaelhartl.com\">Michael Hartl</a>\n  </small>\n  <nav>\n    <ul>\n      <li><a href=\"/about\">About</a></li>\n      <li><a href=\"/contact\">Contact</a></li>\n      <li><a href=\"http://news.railstutorial.org/\">News</a></li>\n    </ul>\n  </nav>\n</footer>\n      \n    </div>\n  </body>\n</html>\n".
        test/integration/following_test.rb:65:in `block (2 levels) in <class:FollowingTest>'
        test/integration/following_test.rb:63:in `block in <class:FollowingTest>'

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only============                        ] 80% 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)
  76/76: [===================================================================================================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.62458s
76 tests, 339 assertions, 1 failures, 0 errors, 0 skips

おわりに

ついにRailsチュートリアルのアプリケーションが完成しました! 4月からこつこつ続けてきて約4ヶ月で完走です。 RubyもRailsも知識ゼロでしたが、自分の手で作り上げていくのはとても楽しかったです。

新しいことを学ぶのに、年齢は関係ないですね。

素晴らしい教材を提供してくださったMichaelさん、安川さんに感謝です!

【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になる理由がわかっていません。 もやもやするな・・・。

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

はじめに

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

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

動作環境

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

14.1.1 データモデルの問題 (および解決策)

本章での学び

【DB設計】各モデルの考え方

ユーザー、ユーザーがフォローしているユーザー、能動的関係の3つを設計する。

ユーザー:userテーブル ユーザーがフォローしているユーザー:user.following(userテーブルの集合) 能動的関係:active_relationshipsテーブル

image

  • 自分からフォローする(能動的関係)
  • 相手からフォローされる(受動的関係)

どちらも1つのテーブルで実現するため、テーブル名はrelationshipsテーブルとなる。

image

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




【DB】マイグレーションファイルの作成

rails generateで、relationshipsテーブルの マイグレーションファイルを自動生成する。

yokoyan:~/workspace/sample_app (following-users) $ rails generate model Relationship follower_id:integer followed_id:integer
^[Running via Spring preloader in process 3357
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170823121831_create_relationships.rb
      create    app/models/relationship.rb
      invoke    test_unit
      create      test/models/relationship_test.rb
      create      test/fixtures/relationships.yml

【DB】マイグレーションファイルにインデックスを追加

自動生成したマイグレーションファイルに、インデックスを追加する。 [:follower_id, :followed_id], unique: trueは複合キー。 2つのidの組み合わせが必ずユニークであることを保証する。 (同じユーザーを2回以上フォローできないようにする)

class CreateRelationships < ActiveRecord::Migration[5.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

【DB】マイグレーションの実行

relationshipsテーブルを作成する。

yokoyan:~/workspace/sample_app (following-users) $ rails db:migrate
== 20170823121831 CreateRelationships: migrating ==============================
-- create_table(:relationships)
   -> 0.0094s
== 20170823121831 CreateRelationships: migrated (0.0095s) =====================

演習1

図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。

id=1がフォローしているユーザーの配列が取得できる。 (id=2,7,8,10のユーザー配列が取得できる)

演習2

図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。

id=2のユーザーは既にフォローしているのでフォローできない(またはフォローが解除される) id=1がフォローしているユーザーの配列が取得できる。

14.1.2 User/Relationshipの関連付け

本章での学び

【model】能動的関係に対して1対多(has_many)の関連付けを実装する

userモデルをリファクタリングする。 userモデルとrelationshipsモデルは1対多の関係となる。 能動的関係であることを示すために、has_many :active_relationshipと名前をつけている。 明示的にクラス名と、外部キーの名称を指定している。

また、userを削除した場合、user間のrelationshipも削除されるように、 dependent :destroyを付与している。

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name: "Relationship",
                                  foreign_key: "follower_id",
                                  dependent: :destroy

【model】リレーションシップ/フォロワーに対してbelongs_toの関連付けを追加する

relationshipモデルと、フォローされているuserモデル(またはフォローしているuserモデル)は1対1となるため、belongs_toで関連付けを行う。

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

今回使えるようになったメソッド

image

演習1

コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。

最初のユーザーが2人目のユーザーをフォローする。

yokoyan:~/workspace/sample_app (following-users) $ rails console --sandbox
Running via Spring preloader in process 2424
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>> user = User.first
  User Load (0.5ms)  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-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>
>> 
?> other_user = User.second
  User Load (0.4ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Chester Greenfelder III", email: "example-1@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$OEftiG18yO2djfYF/Qd2yODa4aYbgFbLpgWRhX9zw/....", remember_digest: nil, admin: false, activation_digest: "$2a$10$tnRwE2WFuUJiQ4BlKEXbne1h2Ejr.8LrUPA7FY.WQMw...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>> 
?> active_relationship = user.active_relationships.create(followed_id: other_user.id)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.3ms)  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 (0.5ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", 2017-08-24 10:52:28 UTC], ["updated_at", 2017-08-24 10:52:28 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2017-08-24 10:52:28", updated_at: "2017-08-24 10:52:28">

演習2

先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。

active_relationship.followedは、自分がフォローしているユーザを返す。 つまり、user_id=1のユーザが、user_id=2のユーザをフォローしているので、User id:2が返ってくる。

active_relationship.followerは、フォロワーを返す。 つまり、user_id=2のユーザのフォロワーである、User id:1が返ってくる。

?> active_relationship.followed
=> #<User id: 2, name: "Chester Greenfelder III", email: "example-1@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$OEftiG18yO2djfYF/Qd2yODa4aYbgFbLpgWRhX9zw/....", remember_digest: nil, admin: false, activation_digest: "$2a$10$tnRwE2WFuUJiQ4BlKEXbne1h2Ejr.8LrUPA7FY.WQMw...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>> 
?> active_relationship.follower
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>

14.1.3 Relationshipのバリデーション

本章での学び

【test】Relationshipモデルのバリデーション

  • setup

    • Relationshipモデルを生成する(followed_id: michael,follower_id: archer)
    • インスタンス変数@relationshipに代入する
  • should be valid

    • @relationshipが有効であること
  • should require a follower_id

    • @relationshipのfollower_idにnilを代入
    • @relationshipが有効ではないこと
  • should require a followed_id

    • @relationshipのfollowed_idにnilを代入
    • @relationshipが有効ではないこと

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

  test "should be valid" do
    assert @relationship.valid?
  end

  test "should require a follower_id" do
    @relationship.follower_id = nil
    assert_not @relationship.valid?
  end

  test "should require a followed_id" do
    @relationship.followed_id = nil
    assert_not @relationship.valid?
  end

【model】 Relationshipモデルに対してバリデーションを追加する

  validates :follower_id, presence: true
  validates :followed_id, presence: true

【fixture】Relationship用のfixtureを空にする

自動生成されたfixtureでは、Relationshipモデルの複合キーの一意性を満たせないため、 コメントアウトする。

# one:
#   follower_id: 1
#   followed_id: 1

# two:
#   follower_id: 1
#   followed_id: 1

演習1

リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)

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

  # validates :follower_id, presence: true
  # validates :followed_id, presence: true

コメントアウトしてもテスト結果がgreenになることを確認。

14.1.4 フォローしているユーザー

本章での学び

【model】 Userモデルにfollowingの関連付けを追加

多対多の関係性を表すために、has_many throughを使う。 Railsは、デフォルトでは、has_manyに指定された単数形のモデル名に対応する外部キーを探す。 そのため、:sourceパラメータを使って、「following配列の元はfollowed_idの集合である」ことを明示的にする。

  has_many :following, through: :active_relationships, source: :followed

【test】following関連のメソッドのテスト

  • should follow and unfollow a user
    • fixtureからmichaelユーザ情報を取得
    • fixtureからarcherユーザ情報を取得
    • michaelが、archerをフォローしていないことを確認
    • michaelが、archerをフォローする
    • michaelが、archerをフォローしていることを確認
    • michaelが、archerをフォロー解除する
    • michaelが、archerをフォローしていないことを確認

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

  test "should follow and unfollow a user" do
    michael = users(:michael)
    archer = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end

【model】following関連のメソッドの実装

テストコードで作成したメソッドの中身を実装する。

  # ユーザーをフォローする
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end

  # ユーザーをフォロー解除する
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # 現在のユーザーがフォローしていたらtrueを返す
  def following?(other_user)
    following.include?(other_user)
  end

テスト実行

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

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

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                                       ] 9% Time: 00:00:01,  ETA: 00:00:16
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)
  63/63: [=====================================================================================================================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.80719s
63 tests, 268 assertions, 0 failures, 0 errors, 0 skips

演習1

コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。

実行結果は以下の通り。

yokoyan:~/workspace/sample_app (following-users) $ rails console --sandbox
Running via Spring preloader in process 4373
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>>
?> michael = User.first
  User Load (0.3ms)  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-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>
>>
?> archer = User.second
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Chester Greenfelder III", email: "example-1@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$OEftiG18yO2djfYF/Qd2yODa4aYbgFbLpgWRhX9zw/....", remember_digest: nil, admin: false, activation_digest: "$2a$10$tnRwE2WFuUJiQ4BlKEXbne1h2Ejr.8LrUPA7FY.WQMw...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>>
?> michael.following?(archer)
  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]]
=> false
>>
?> michael.follow(archer)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.4ms)  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 (2.0ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", 2017-08-26 00:55:42 UTC], ["updated_at", 2017-08-26 00:55:42 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2017-08-26 00:55:42", updated_at: "2017-08-26 00:55:42">
>> 
?> michael.following?(archer)
  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]]
=> true
>> 
?> michael.unfollow(archer)
  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]]
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.4ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2017-08-26 00:55:42", updated_at: "2017-08-26 00:55:42">
>> 
?> michael.following?(archer)
  User Exists (0.2ms)  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]]
=> false

演習2

先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。 演習1の結果参照。

14.1.5 フォロワー

本章での学び

フォロワー機能を実装する。

【model】受動的関係のデータモデルの考え方

relationshipsテーブルを使う。 Michael(id=1)は、3人のユーザのにフォローされている。(id=3,2,9) followed_id=1と、follower_id=3,2,9

image

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

source: :followerを省略することも可能。 has_many :followersと記述した場合、Railsは自動的にfollowersを 単数形のfollowerに変換して、follower_idを探してくれる。

今回は、has_many :followingとの類似性を強調するために、 あえて記述している。

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name: "Relationship",
                                  foreign_key: "follower_id",
                                  dependent: :destroy
  has_many :passive_relationships, class_name: "Relationship",
                                  foreign_key: "followed_id",
                                  dependent: :destroy
  has_many :following, through: :active_relationships, source: :followed
  has_many :followers, through: :passive_relationships, source: :follower

【test】followersに対するテスト

  • should follow and unfollow a user
    • fixtureからmichaelユーザ情報を取得
    • fixtureからarcherユーザ情報を取得
    • michaelが、archerをフォローしていないことを確認
    • michaelが、archerをフォローする
    • michaelが、archerをフォローしていることを確認
    • archerのフォロワーに、michaelが存在することを確認
    • michaelが、archerをフォロー解除する
    • michaelが、archerをフォローしていないことを確認

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

  test "should follow and unfollow a user" do
    michael = users(:michael)
    archer = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    assert archer.follower.include?(michael)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end

動作確認

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

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

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only=======================                               ] 77% 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)
  63/63: [=====================================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.26879s
63 tests, 269 assertions, 0 failures, 0 errors, 0 skips

演習1

コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?

最初のユーザから、4番目までのユーザを作成する。

yokoyan:~/workspace/sample_app (following-users) $ rails console --sandbox
Running via Spring preloader in process 6034
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>> 
?> user = User.first
  User Load (0.6ms)  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-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>
>> 
?> user2 = User.second
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Chester Greenfelder III", email: "example-1@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$OEftiG18yO2djfYF/Qd2yODa4aYbgFbLpgWRhX9zw/....", remember_digest: nil, admin: false, activation_digest: "$2a$10$tnRwE2WFuUJiQ4BlKEXbne1h2Ejr.8LrUPA7FY.WQMw...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>> 
u?> user3 = User.third
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 2]]
=> #<User id: 3, name: "Chad Runolfsdottir", email: "example-2@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$JobCCI0QOmLfs6UN4SykdeCm4qGJv29oIWahAIxIAuR...", remember_digest: nil, admin: false, activation_digest: "$2a$10$ln97hpFaoM4p8escgCyvVO30Sh.Z7h2ZQoGPCDMfcao...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>> 
?> user4 = User.fourth
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 3]]
=> #<User id: 4, name: "Alexandro Lemke", email: "example-3@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$9bcHizLobKZITlg1egPVy.z0R1AUN800245UXxH71Ct...", remember_digest: nil, admin: false, activation_digest: "$2a$10$lQQRPpzraXZ/uQ/x8FFhyunDpLaLlpMX6gB5O2b4fML...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>

3人のユーザが最初のユーザーをフォローする。

>> 
?> user2.follow(user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (0.9ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 2], ["followed_id", 1], ["created_at", 2017-08-26 04:05:32 UTC], ["updated_at", 2017-08-26 04:05:32 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 2, followed_id: 1, created_at: "2017-08-26 04:05:32", updated_at: "2017-08-26 04:05:32">
>> 
?> user3.follow(user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (0.5ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 3], ["followed_id", 1], ["created_at", 2017-08-26 04:05:41 UTC], ["updated_at", 2017-08-26 04:05:41 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 2, follower_id: 3, followed_id: 1, created_at: "2017-08-26 04:05:41", updated_at: "2017-08-26 04:05:41">
>> 
?> user4.follow(user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (0.3ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 4], ["followed_id", 1], ["created_at", 2017-08-26 04:05:53 UTC], ["updated_at", 2017-08-26 04:05:53 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 3, follower_id: 4, followed_id: 1, created_at: "2017-08-26 04:05:53", updated_at: "2017-08-26 04:05:53">

最初のユーザーのフォロー数を確認する。 3人のユーザからフォローされていることを確認。

?> user.followers.map(&:id)
  User Load (0.4ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> [2, 3, 4]

演習2

*上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。

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

演習3

user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。

user.followers.to_a.countでも、同じく3が返ってくる。 100万人のフォロワーがいた場合、100万人分の配列を生成してその数を返す。 配列を生成する分、実行コストがかかると思われる。

一方、user.followers.countは、Railsは高速化のためにDB内で合計を計算する。

?> user.followers.to_a.count
=> 3

おわりに

ついに最後の章に入りました! これまでのモデルに比べると、多対多の関係となっているため複雑です。 モデル間の構造を意識しながら、これからもプログラムを書いていきます。

【13章】Railsチュートリアル中にNameError: uninitialized constant Micropost::PictureUploaderが出た時の対処法

発生した章

13.4.1 基本的な画像アップロードにて、CarrierWaveをインストールして、リスト 13.59: Micropostモデルに画像を追加するの通り修正したら、テストでエラーが出るようになった。

やったこと

CarrierWaveに画像と関連付けたモデルを伝えるために、mount_uploaderメソッドを使用する。引数に、属性名(今回はpicture)と、クラス名(PictureUploader)を指定する。

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  mount_uploader :picture, PictureUploader
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

発生したエラー内容

Micropostモデルを呼び出した際に、以下のエラーが発生する。 NameError: uninitialized constant Micropost::PictureUploader

チュートリアルでは、この時点でのtestはgreenになると書かれているが、redになる。 (Micropostモデルから、mount_uploaderメソッドを削除するとgreenになる)

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 9762
Started with run options --seed 39087

ERROR["test_profile_display", UsersProfileTest, 0.09379067993722856]
 test_profile_display#UsersProfileTest (0.09s)
NameError:         NameError: uninitialized constant Micropost::PictureUploader
            app/models/micropost.rb:4:in `<class:Micropost>'
            app/models/micropost.rb:1:in `<top (required)>'

 FAIL["test_content_should_be_at_most_140_characters", MicropostTest, 2.966606823960319]
 test_content_should_be_at_most_140_characters#MicropostTest (2.97s)
        Expected true to be nil or false
        test/models/micropost_test.rb:32:in `block in <class:MicropostTest>'

 FAIL["test_user_id_should_be_present", MicropostTest, 2.974773451918736]
 test_user_id_should_be_present#MicropostTest (2.97s)
        Expected true to be nil or false
        test/models/micropost_test.rb:22:in `block in <class:MicropostTest>'

 FAIL["test_content_should_be_present", MicropostTest, 2.9872157878708094]
 test_content_should_be_present#MicropostTest (2.99s)
        Expected true to be nil or false
        test/models/micropost_test.rb:27:in `block in <class:MicropostTest>'

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.539895322872326]
 test_micropost_interface#MicropostsInterfaceTest (3.54s)
        "Micropost.count" didn't change by 0.
        Expected: 38
          Actual: 39
        test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 3.68161s
59 tests, 245 assertions, 4 failures, 1 errors, 0 skips

参考情報

NameError: uninitialized constant Article::ImageUploader when using Carrierwave on rails 4.1.5

上記の記事を見ると、config/environment.rbの行末に、require 'carrierwave/orm/activerecord'を追記して解決したという人がいる。 他にも、springを再起動したら治ったという人もいる。

対策

springを再起動する。

yokoyan:~/workspace/sample_app (user-microposts) $ spring stop
Spring stopped.
yokoyan:~/workspace/sample_app (user-microposts) $ 
yokoyan:~/workspace/sample_app (user-microposts) $ spring start
Version: 1.7.2

Usage: spring COMMAND [ARGS]

Commands for spring itself:

  binstub         Generate spring based binstubs. Use --all to generate a binstub for all known commands. Use --remove to revert.
  help            Print available commands.
  server          Explicitly start a Spring server in the foreground
  status          Show current status.
  stop            Stop all spring processes for this project.

Commands for your application:

  rails           Run a rails command. The following sub commands will use spring: console, runner, generate, destroy, test.
  rake            Runs the rake command

再度テストを実行。 無事にエラーが解決しました。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 13599
Started with run options --seed 41861

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only================                   ] 84% Time: 00:00:02,  ETA: 00:00:00
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:25)
  59/59: [==================================================================================================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.52173s
59 tests, 262 assertions, 0 failures, 0 errors, 0 skips

【13章】Ruby on Railsチュートリアル演習まとめ&解答例【13.4 マイクロポストの画像投稿】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 13章 13.4 マイクロポストの画像投稿の演習まとめ&解答例です。

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

動作環境

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

13.4 マイクロポストの画像投稿

本章での学び

画像付きマイクロポストを投稿できるようにする。

13.4.1 基本的な画像アップロード

本章での学び

【事前準備】CarrierWaveのインストール

画像アップローダーであるCarrierWaveをGemfileに追加する。 また、画像処理を行うmini_magickと、クラウド環境へアップロードするためのfogも追加する。

gem 'carrierwave', '1.1.0'
gem 'mini_magick', '4.7.0'
gem 'fog', '1.40.0'

インストール。

yokoyan:~/workspace/sample_app (user-microposts) $ bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/..........
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies...
Using rake 12.0.0
Installing CFPropertyList 2.3.5
Using concurrent-ruby 1.0.5
Using i18n 0.8.1
Using minitest 5.10.1
Using thread_safe 0.3.6
Using builder 3.2.3
Using erubis 2.7.0
Using mini_portile2 2.1.0
Using rack 2.0.1
Using nio4r 1.2.1
Using websocket-extensions 0.1.2
Using mime-types-data 3.2016.0521
Using arel 7.1.4
Using ansi 1.5.0
Using execjs 2.7.0
Using bcrypt 3.1.11
Using sass 3.4.23
Using will_paginate 3.1.0
Using bundler 1.14.6
Using byebug 9.0.0
Using coderay 1.1.1
Using coffee-script-source 1.12.2
Using method_source 0.8.2
Using thor 0.19.4
Using debug_inspector 0.0.2
Installing excon 0.58.0
Using ffi 1.9.18
Using formatador 0.2.5
Using multi_json 1.12.1
Installing ipaddress 0.8.3
Installing xml-simple 1.1.5
Installing inflecto 0.0.2
Installing json 1.8.6 with native extensions
Installing trollop 2.1.2
Using rb-fsevent 0.9.8
Using lumberjack 1.0.11
Using nenv 0.3.0
Using shellany 0.0.1
Using slop 3.6.0
Using guard-compat 1.2.1
Installing mini_magick 4.7.0
Using ruby-progressbar 1.8.1
Using puma 3.4.0
Using tilt 2.0.6
Using spring 1.7.2
Using sqlite3 1.3.11
Using turbolinks-source 5.0.0
Installing fission 0.5.0
Using faker 1.6.6
Using tzinfo 1.2.2
Using nokogiri 1.7.0.1
Using rack-test 0.6.3
Using sprockets 3.7.1
Using websocket-driver 0.6.5
Using mime-types 3.1
Using autoprefixer-rails 6.7.7.1
Using uglifier 3.0.0
Using bootstrap-will_paginate 0.0.10
Using coffee-script 2.4.1
Using rb-inotify 0.9.8
Installing fog-core 1.45.0
Using notiffany 0.1.1
Using pry 0.10.4
Using guard-minitest 2.4.4
Using minitest-reporters 1.1.9
Using turbolinks 5.0.1
Using activesupport 5.0.0.1
Using loofah 2.0.3
Installing rbvmomi 1.11.3
Using mail 2.6.4
Using bootstrap-sass 3.3.6
Using listen 3.0.8
Installing fog-json 1.0.2
Installing fog-xml 0.1.3
Installing fog-local 0.3.1
Installing fog-vmfusion 0.1.0
Using rails-dom-testing 2.0.2
Using globalid 0.3.7
Using activemodel 5.0.0.1
Using jbuilder 2.4.1
Using rails-html-sanitizer 1.0.3
Installing fog-vsphere 1.11.3
Using guard 2.13.0
Using spring-watcher-listen 2.0.0
Installing fog-aliyun 0.2.0
Installing fog-brightbox 0.13.0
Installing fog-dnsimple 1.0.0
Installing fog-openstack 0.1.21
Installing fog-profitbricks 4.0.0
Installing fog-sakuracloud 1.7.5
Installing fog-serverlove 0.1.2
Installing fog-softlayer 1.1.4
Installing fog-storm_on_demand 0.1.1
Installing fog-atmos 0.1.0
Installing fog-aws 1.4.0
Installing fog-cloudatcost 0.1.2
Installing fog-digitalocean 0.3.0
Installing fog-dynect 0.0.3
Installing fog-ecloud 0.3.0
Installing fog-google 0.1.0
Installing fog-powerdns 0.1.1
Installing fog-rackspace 0.1.5
Installing fog-radosgw 0.0.5
Installing fog-riakcs 0.1.0
Installing fog-terremark 0.1.0
Installing fog-voxel 0.1.0
Installing fog-xenserver 0.3.0
Using activejob 5.0.0.1
Using activerecord 5.0.0.1
Installing carrierwave 1.1.0
Using actionview 5.0.0.1
Installing fog 1.40.0
Using actionpack 5.0.0.1
Using actioncable 5.0.0.1
Using actionmailer 5.0.0.1
Using railties 5.0.0.1
Using sprockets-rails 3.2.0
Using rails-controller-testing 0.1.1
Using coffee-rails 4.2.1
Using jquery-rails 4.1.1
Using web-console 3.1.1
Using rails 5.0.0.1
Using sass-rails 5.0.6
Bundle complete! 28 Gemfile dependencies, 124 gems now installed.
Gems in the group production were not installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
Post-install message from fog:
------------------------------
Thank you for installing fog!

IMPORTANT NOTICE:
If there's a metagem available for your cloud provider, e.g. `fog-aws`,
you should be using it instead of requiring the full fog collection to avoid
unnecessary dependencies.

'fog' should be required explicitly only if:
- The provider you use doesn't yet have a metagem available.
- You require Ruby 1.9.3 support.
------------------------------

【事前準備】画像アップローダーの作成

Railsのジェネレーターで画像アップローダーを生成する。

yokoyan:~/workspace/sample_app (user-microposts) $ rails generate uploader Picture
Running via Spring preloader in process 9141
Expected string default value for '--jbuilder'; got true (boolean)
      create  app/uploaders/picture_uploader.rb

【事前準備】DBのマイグレーション

自動生成したPictureとマイクロポストを関連付けるために、 Micropostモデルにpicture属性を追加する。

yokoyan:~/workspace/sample_app (user-microposts) $ rails generate migration add_picture_to_microposts picture:string
Running via Spring preloader in process 9155
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170820040443_add_picture_to_microposts.rb

自動生成されたマイグレーションファイル。

class AddPictureToMicroposts < ActiveRecord::Migration[5.0]
  def change
    add_column :microposts, :picture, :string
  end
end

マイグレーションの実行。

yokoyan:~/workspace/sample_app (user-microposts) $ rails db:migrate
== 20170820040443 AddPictureToMicroposts: migrating ===========================
-- add_column(:microposts, :picture, :string)
   -> 0.0048s
== 20170820040443 AddPictureToMicroposts: migrated (0.0050s) ==================

【model】Micropostモデルに画像を追加する

CarrierWaveに画像と関連付けたモデルを伝えるために、mount_uploaderメソッドを使用する。引数に、属性名(今回はpicture)と、クラス名(PictureUploader)を指定する。

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  mount_uploader :picture, PictureUploader
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

チュートリアルでは、この時点でのtestはgreenになると書かれているが、redになる。 (Micropostモデルから、mount_uploaderメソッドを削除するとgreenになる)

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 9762
Started with run options --seed 39087

ERROR["test_profile_display", UsersProfileTest, 0.09379067993722856]
 test_profile_display#UsersProfileTest (0.09s)
NameError:         NameError: uninitialized constant Micropost::PictureUploader
            app/models/micropost.rb:4:in `<class:Micropost>'
            app/models/micropost.rb:1:in `<top (required)>'

 FAIL["test_content_should_be_at_most_140_characters", MicropostTest, 2.966606823960319]
 test_content_should_be_at_most_140_characters#MicropostTest (2.97s)
        Expected true to be nil or false
        test/models/micropost_test.rb:32:in `block in <class:MicropostTest>'

 FAIL["test_user_id_should_be_present", MicropostTest, 2.974773451918736]
 test_user_id_should_be_present#MicropostTest (2.97s)
        Expected true to be nil or false
        test/models/micropost_test.rb:22:in `block in <class:MicropostTest>'

 FAIL["test_content_should_be_present", MicropostTest, 2.9872157878708094]
 test_content_should_be_present#MicropostTest (2.99s)
        Expected true to be nil or false
        test/models/micropost_test.rb:27:in `block in <class:MicropostTest>'

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.539895322872326]
 test_micropost_interface#MicropostsInterfaceTest (3.54s)
        "Micropost.count" didn't change by 0.
        Expected: 38
          Actual: 39
        test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 3.68161s
59 tests, 245 assertions, 4 failures, 1 errors, 0 skips

一旦先に進めることにする。

【view】パーシャルに画像を追加する。

f.file_field属性を使って、マイクロポスト投稿フォームに画像を表示する。

file_fieldは、ファイル選択ボックスを表示するとのこと。

Railsドキュメント file_field

<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture %>
  </span>
<% end %>

【controller】Micropostsコントローラのリファクタリング

picture属性をブラウザから更新できる許可リストに追加する。

  private

    def micropost_params
      params.require(:micropost).permit(:content, :picture)
    end

【view】マイクロポスト表示パーシャルのリファクタリング

image_tagヘルパーを使って、マイクロポストの表示項目に画像を追加する。 画像が存在する場合のみ、画像を表示する。

image_tagヘルパーは以下参照。 Railsドキュメント image_tag

  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
  </span>

演習1

画像付きのマイクロポストを投稿してみましょう。もしかして、大きすぎる画像が表示されてしまいましたか? (心配しないでください、次の13.4.3でこの問題を直します)。

画像がアップロードできることを確認。 image

演習2

リスト 13.63に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください (コマンド例: cp app/assets/images/rails.png test/fixtures/)。リスト 13.63で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです18。ヒント: picture属性が有効かどうかを確かめるときは、11.3.3で紹介したassignsメソッドを使ってください。このメソッドを使うと、投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。

サンプル画像をfixtureディレクトリに配置する。

yokoyan:~/workspace/sample_app (user-microposts) $ cp app/assets/images/rails.png test/fixtures/

テストを追加する。

  • HTML属性に、input type=file属性があることを確認
  • fixtureディレクトリにあるrails.pngを、image/pngにアップロードして、picture変数に代入
  • マイクロポスト投稿時に、picture変数を引数に追加して投稿できることを確認
  • マイクロポストにpictureが存在することを確認

※しれっと、params:ハッシュがなくなっているので注意。

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

13.4.2 画像の検証

本章での学び

画像サイズや拡張子に対するバリデーションを実装する。

画像フォーマットのバリデーション追加

extension_whitelistメソッドのコメントアウトを外す。

  def extension_whitelist
    %w(jpg jpeg gif png)
  end

【model】画像サイズのバリデーション追加

画像のサイズを制限する独自のバリデーションを追加する。 独自のバリデーションを定義するためには、validatesメソッドではなく、validateメソッドを使用する。 5MBを超えた場合はエラーメッセージを表示する。

  validate  :picture_size
  
  private
  
    # アップロードされた画像サイズをバリデーションする
    def picture_size
      if picture.size > 5.megabytes
        errors.add(:picture, "should be less than 5MB")
      end
    end

【view】パーシャルにファイルサイズチェックを追加する

マイクロポスト投稿フォームに、画像の拡張子の許可と、ファイルサイズチェックを行うjQuery関数を追加する。

jQuery関数では、ブラウザのインスペクタ機能でjavaScriptを直接修正したり、curlで直接POSTしてきた場合、防ぐことができない。そのために前項で実装したサーバサイド側のバリデーションも必要となる。

<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png'%>
  </span>
<% end %>

<script type="text/javascript">
  $('#micropost_picture').bind('change', function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 5) {
      alert('Maximum file size is 5MB. Please choose a smaller file.')
    }
  });
</script>

演習1

5MB以上の画像ファイルを送信しようとした場合、どうなりますか?

jQueryの警告が表示されアップロードできない。

image

演習2

無効な拡張子のファイルを送信しようとした場合、どうなりますか?

ホワイトリストのエラーメッセージが表示される。

image

13.4.3 画像のリサイズ

本章での学び

画像のリサイズ処理を実装する。

【事前準備】ImageMagickのインストール

ImageMagickをインストールする。

yokoyan:~/workspace/sample_app (user-microposts) $ sudo apt-get update
yokoyan:~/workspace/sample_app (user-microposts) $ sudo apt-get install imagemagick --fix-missing

アップローダーのリファクタリング

MiniMagickというImageMagickとRubyをつなぐgemを使って画像をリサイズする。 縦横どちらかが400pxを超えていた場合、適切なサイズにリサイズする。

class PictureUploader < CarrierWave::Uploader::Base

  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  include CarrierWave::MiniMagick
  process reseize_to_limit: [400, 400]

演習1

解像度の高い画像をアップロードし、リサイズされているかどうか確認してみましょう。画像が長方形だった場合、リサイズはうまく行われているでしょうか?

上記の実装をした後、動作確認を行うとtranslation missing: en.errors.messages.mini_magick_processing_errorエラーが発生する。 以下の人と同じ現象。未解決。

Image translation missing: en.errors.messages.mini_magick_processing_error #2102

演習2

既にリスト 13.63のテストを追加していた場合、この時点でテストスイートを走らせるとエラーメッセージが表示されるようになるはずです。このエラーを取り除いてみましょう。ヒント: リスト 13.68にある設定ファイルを修正し、テスト時はCarrierWaveに画像のリサイズをさせないようにしてみましょう。

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

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 3120
Started with run options --seed 50042

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only=======================================================================                                ] 83% Time: 00:00:02,  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)
 FAIL["test_micropost_interface", MicropostsInterfaceTest, 2.907269152114168]
 test_micropost_interface#MicropostsInterfaceTest (2.91s)
        "Micropost.count" didn't change by 1.
        Expected: 39
          Actual: 38
        test/integration/microposts_interface_test.rb:25:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 3.41207s
59 tests, 256 assertions, 1 failures, 0 errors, 0 skips

設定ファイルを生成する。

yokoyan:~/workspace/sample_app (user-microposts) $ touch config/initializers/skip_image_resizing.rb

テスト時はリサイズ処理をスキップする。

if Rails.env.test?
  CarrierWave.configure do |config|
    config.enable_processing = false
  end
end

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

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 1833
Started with run options --seed 2677

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only==========                         ] 79% Time: 00:00:02,  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)
  59/59: [==================================================================================================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.69379s
59 tests, 262 assertions, 0 failures, 0 errors, 0 skips

13.4.4 本番環境での画像アップロード

本章での学び

設定の切り替え

本番環境ではfog gemを使って、AWSのS3に画像をアップロードできるようにする。

  if Rails.env.production?
    storage :fog
  else
    storage :file
  end

【事前準備】AWSのIAM設定

IAMユーザを追加する。

image.png

プログラムによるアクセスにチェックを入れる。

image.png

ユーザーを作成して、AccessキーとSecretキーを取得する。

【事前準備】S3バケットの作成

任意のバケットを作成する。 バケット名はユニークにする必要があるので注意。 リージョンは アジアパシフィック (東京)を選択する。

image.png

他の設定はデフォルトのまま。

image.png

パブリックアクセスは付与しない。

image.png

バケットの完成。

image.png

【事前準備】CarrierWaveの設定ファイル作成

本番環境の場合に使用する、carrier_waveの設定ファイルを作成する。

yokoyan:~/workspace/sample_app (user-microposts) $ touch config/initializers/carrier_wave.rb
if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_credentials = {
      # Amazon S3用の設定
      :provider              => 'AWS',
      :region                => ENV['S3_REGION'],
      :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
      :aws_secret_access_key => ENV['S3_SECRET_KEY']
    }
    config.fog_directory = ENV['S3_BUCKET']
  end
end

【事前準備】heroku側のAWS設定

Herokuの環境変数設定を行う。

$ heroku config:set S3_ACCESS_KEY="ココに先ほどメモしたAccessキーを入力"
$ heroku config:set S3_SECRET_KEY="同様に、Secretキーを入力"
$ heroku config:set S3_BUCKET="Bucketの名前を入力"
$ heroku config:set S3_REGION="Regionの名前を入力"

【事前準備】gitignoreの編集

アップロードされたテスト画像が、gitで管理されないように、.gitignoreに追加する。

# アップロードされたテスト画像を無視する
/public/uploads

gitへのコミット、マージ

commit、mergeを行い、gitへpushする。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
yokoyan:~/workspace/sample_app (user-microposts) $ git add -A
yokoyan:~/workspace/sample_app (user-microposts) $ git commit -m "Add user microposts"
yokoyan:~/workspace/sample_app (user-microposts) $ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
yokoyan:~/workspace/sample_app (master) $ git merge user-microposts 
yokoyan:~/workspace/sample_app (master) $ git push

本番環境へのリリース

Herokuへのデプロイを行う。

yokoyan:~/workspace/sample_app (master) $ git push heroku

データベースのリセットを行う。

yokoyan:~/workspace/sample_app (master) $ heroku pg:reset DATABASE
 ▸    heroku-cli: This CLI is deprecated. Please reinstall from https://cli.heroku.com
 ▸    WARNING: Destructive action
 ▸    postgresql-infinite-44520 will lose all of its data
 ▸    
 ▸    To proceed, type enigmatic-everglades-70434 or re-run this command with --confirm enigmatic-everglades-70434

> enigmatic-everglades-70434
Resetting postgresql-infinite-44520... done

データベースのマイグレーションを行う。

yokoyan:~/workspace/sample_app (master) $ heroku run rails db:migrate
 ▸    heroku-cli: This CLI is deprecated. Please reinstall from https://cli.heroku.com
Running rails db:migrate on ⬢ enigmatic-everglades-70434... up, run.7170 (Free)
D, [2017-08-23T04:06:30.854856 #4] DEBUG -- :    (37.2ms)  CREATE TABLE "schema_migrations" ("version" character varying PRIMARY KEY)
・・・略・・・
D, [2017-08-23T04:06:31.159450 #4] DEBUG -- :    (2.2ms)  COMMIT
D, [2017-08-23T04:06:31.161335 #4] DEBUG -- :    (1.7ms)  SELECT pg_advisory_unlock(414146247089748600)

本番環境のデータベースでサンプルデータを生成する。

yokoyan:~/workspace/sample_app (master) $ heroku run rails db:seed
・・・略・・・
D, [2017-08-23T04:08:49.010414 #4] DEBUG -- :    (0.9ms)  BEGIN
D, [2017-08-23T04:08:49.014906 #4] DEBUG -- :   SQL (1.1ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["content", "Recusandae asperiores dicta et consequatur praesentium error officia."], ["user_id", 6], ["created_at", 2017-08-23 04:08:49 UTC], ["updated_at", 2017-08-23 04:08:49 UTC]]
D, [2017-08-23T04:08:49.024077 #4] DEBUG -- :    (8.3ms)  COMMIT

演習1

本番環境で解像度の高い画像をアップロードし、適切にリサイズされているか確認してみましょう。長方形の画像であっても、適切にリサイズされていますか?

本番環境にて解像度の高い画像をアップロードする。 本番環境では問題なくリサイズされていることを確認。

image.png

おわりに

画像の投稿ができるようになり、AWSのS3とも連携できるようになりました。 S3連携は敷居が高いかなと思っていたのですが、fog gemを入れれば簡単に連携することができました。 こういう豊富なgemが用意されているのもRailsの魅力ですね。

ローカルで発生した、translation missing: en.errors.messages.mini_magick_processing_errorエラーは未解決です。 gem側のバグなのか、環境依存の問題なのかの切り分けがまだできていませんが、 後日改めて調査したいと思います。

【13章】Ruby on Railsチュートリアル演習まとめ&解答例【13.3 マイクロポストを操作する】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 13章 13.3 マイクロポストを操作するの演習まとめ&解答例です。

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

動作環境

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

13.3 マイクロポストを操作する

本章での学び

本章では、マイクロポストの作成と削除を行えるようにする。 Micropostsコントローラのアクションは、createとdestroyのみ必要となる。

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

routesコマンドで設定値を確認する。

yokoyan:~/workspace/sample_app (user-microposts) $ rails routes
                 Prefix Verb   URI Pattern                                          
・・・略・・・
              microposts POST   /microposts(.:format)                   microposts#create
              micropost DELETE /microposts/:id(.:format)               microposts#destroy

image

13.3.1 マイクロポストのアクセス制御

本章での学び

Micropostsコントローラのcreateアクションや、destroyアクションは、ログイン済みである必要がある。

【test】Micropostsコントローラの認可テスト

  • setup

    • micropostsのfixtureからorangeを取得する
    • インスタンス変数@micropostに代入する
  • should redirect create when not logged in

    • Micropostモデルの数をカウントし、数が変化していないこと
      • POSTリクエストを送信する
        • microposts_path
        • params
          • micropost
            • content
    • ログイン画面にリダイレクトされること
  • should redirect destroy when not logged in

    • Micropostモデルの数をカウントし、数が変化していないこと
      • DELETEリクエストを送信する
        • micropost_path(@micropost)
    • ログイン画面にリダイレクトされること

上記の仕様で実装する。

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete micropost_path(@micropost)
    end
    assert_redirected_to login_url
  end

【controller】beforeフィルタのリファクタリング

ユーザーがログインしているかどうかを判定するlogged_in_userメソッドは、Usersコントローラだけでなく、Micropostsコントローラからも使用する。

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                    :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

そのため、Usersコントローラ内のlogged_in_userメソッドを、共通の親クラスであるApplicationコントローラに処理を移す。(Javaとは違って、Rubyは継承した親クラス内のprivateメソッドを呼び出せるようだ)

application_controllerにhello, world!のメソッドが残ってる・・・。ここまで長い道のりでした。

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

  def hello
    render html:"hello, world!"
  end

  private

    # ユーザーのログインを確認する
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

リファクタリングが終わったので、Usersコントローラ内のlogged_in_userメソッドは削除する。

【controller】Micropostsコントローラの実装

create、destroyアクションを追加し、前項でリファクタリングしたbeforeフィルタを実装する。

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
  end

  def destroy
  end
end

【test】テストの実行

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

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 2188
Started with run options --seed 11090

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

Finished in 3.56572s
56 tests, 243 assertions, 0 failures, 0 errors, 0 skips

演習1

なぜUsersコントローラ内にあるlogged_in_userフィルターを残したままにするとマズイのでしょうか? 考えてみてください。

Railsの基本理念であるDRY(Don’t repeat yourself)に反するため。 Micropostsコントローラ側でもユーザーのログイン判定を行う必要があり、Usersコントローラ内にlogged_in_userフィルターを残したままにすると、Micropostsコントローラ側にも同じ処理を実装する必要が出てくるため無駄なコードが増えてしまう。

13.3.2 マイクロポストを作成する

本章での学び

【controller】createアクションの実装

Strong parametersで、paramsハッシュでは:micropost属性を必須として、:content属性を許可する。

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

ログインしているユーザーのマイクロポストを生成し、DBに保存する。 成功すればroot_urlへリダイレクトする。 失敗すればstatic_pages配下のhomeページを呼び出す。

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

【View】Homeページのリファクタリング

ユーザーがログインしているかどうかで表示を変える。

  • ログインしている 
    • user_infoパーシャル(未実装)を表示する
    • micropost_formパーシャル(未実装)を表示する
  • ログインしていない
    • Sign upを促す
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
<% else %>
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
    This is the home page for the
    <a href="http://railstutorial.jp">Ruby on Rails Tutorial</a>
    sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary"%>
  </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
              'http://rubyonrails.org/'%>
<% end %>

【View】パーシャルの作成

未実装の2つのパーシャルを作成する。

yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/shared/_user_info.html.erb
yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/shared/_micropost_form.html.erb
ユーザー情報パーシャル

Homeページのサイドバーにユーザー情報を表示する。

  • ユーザーのGravatar画像
  • ユーザー名
  • ユーザープロフィール画面へのリンク
  • ユーザーのマイクロポスト数の合計
    • pluralizeヘルパーで英語の複数形に変換
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost")%></span>
マイクロポスト投稿フォームパーシャル

マイクロポストを投稿するためのフォーム。

  • form_forで、Micropostモデルに紐づいたフォームを作成する
  • エラーメッセージパーシャルを表示
  • テキストエリアのcontent属性を表示
  • submitボタン
<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>

【controller】StaticPagesコントローラのリファクタリング

homeアクションにマイクロポストのインスタンス変数を定義する。 ログインしているユーザのマイクロポストを取得する。

class StaticPagesController < ApplicationController
  def home
    @micropost = current_user.microposts.build if logged_in?
  end

【view】エラーメッセージパーシャルのリファクタリング

form_forで定義する変数が、画面によって変化する。

  • ユーザー登録画面:@user
  • マイクロポスト投稿画面:@micropost

そのため、エラーメッセージパーシャルの呼び出し方法は、 複数のインスタンス変数に対応するためにobject: f.objectとなる。

<% form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>

そのため、エラーメッセージパーシャル内で もともと@user固定で呼び出していた箇所を、object変数を使うように リファクタリングする。

■修正前

<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

■修正後

<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(object.errors.count, "error") %>
    </div>
    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

【test】テスト実行

現時点ではredになってしまう。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 3577
Started with run options --seed 41588

ERROR["test_password_resets", PasswordResetsTest, 2.4003681868780404]
 test_password_resets#PasswordResetsTest (2.40s)
ActionView::Template::Error:         ActionView::Template::Error: undefined local variable or method `object' for #<#<Class:0x007efdd02d7e20>:0x00000005ff8f20>
        Did you mean?  object_id
            app/views/shared/_error_messages.html.erb:1:in `_app_views_shared__error_messages_html_erb__4293521809018927093_50498580'
            app/views/password_resets/edit.html.erb:7:in `block in _app_views_password_resets_edit_html_erb__1534623624385800471_50317940'
            app/views/password_resets/edit.html.erb:6:in `_app_views_password_resets_edit_html_erb__1534623624385800471_50317940'
            test/integration/password_resets_test.rb:40:in `block in <class:PasswordResetsTest>'

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

Finished in 2.83377s
56 tests, 234 assertions, 0 failures, 1 errors, 0 skips

【view】エラーメッセージパーシャルの呼び出し元画面のリファクタリング

1.ユーザー登録画面、ユーザー編集画面

この2画面は共通のフォームを使用しているため、object:@userの箇所を、 object: f.objectにリファクタリングする。

    <%= form_for(@user, url: yield(:url)) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>
2.パスワード再登録画面

エラーメッセージパーシャルの呼び出し部分に、object: f.objectを追加する。

・・・略・・・
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

【test】再度テストを実行

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

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 4197
Started with run options --seed 46999

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

Finished in 3.34028s
56 tests, 243 assertions, 0 failures, 0 errors, 0 skips

ブラウザからアクセスできることを確認。

image

演習1

Homeページをリファクタリングして、if-else文の分岐のそれぞれに対してパーシャルを作ってみましょう。

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

yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/static_pages/_home_logged_in.html.erb
yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/static_pages/_home_not_logged_in.html.erb

ログイン時に表示するパーシャル。

  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>

未ログイン時に表示するパーシャル。

  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
    This is the home page for the
    <a href="http://railstutorial.jp">Ruby on Rails Tutorial</a>
    sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary"%>
  </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
              'http://rubyonrails.org/'%>

リファクタリング後のhomeページ。 かなりスッキリしました。

<% if logged_in? %>
  <%= render 'static_pages/home_logged_in' %>
<% else %>
  <%= render 'static_pages/home_not_logged_in' %>
<% end %>

リファクタリング後もtestがgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test 
Running via Spring preloader in process 5658
Started with run options --seed 10843

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

Finished in 3.49162s
56 tests, 243 assertions, 0 failures, 0 errors, 0 skips

13.3.3 フィードの原型

本章での学び

ユーザープロフィール画面に、ユーザーのマイクロポストのフィードを追加する。

【model】Userモデルにfeedメソッドを実装する

すべてのユーザーがフィードを持つため、Userモデルにfeedメソッドを追加する。 SQLインジェクション対策のために、プリペアドステートメントを使う。 (SQLのwhere句にuser.idが入る際にエスケープされる)

  # 試作feedの定義
  # 完全な実装は次章の「ユーザーをフォローする」を参照
  def feed
    Micropost.where("user_id = ?", id)
  end

【controller】static_pagesコントローラのリファクタリング

homeアクションのリファクタリングを行う。 1行だった後置if文を、前置if文に変更する。 また、前項で実装したfeedメソッドの取得結果をインスタンス変数@feed_itemsに格納する。

  def home
    if logged_in?
      @micropost = current_user.microposts.build if logged_in?
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end

【view】フィード表示パーシャルを作成する

インスタンス変数@feed_itemsを表示するためのパーシャルを作成する。 @feed_items内の要素は、Micropostクラスを保持しているため、Micropostのパーシャルを呼び出すことができる。 (app/view/microposts/_micropost.html.erb)

yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
    <%= will_paginate @feed_items %>
<% end %>

【view】homeページヘの組み込み

13.3.2の演習で作成したパーシャルから、フィード表示パーシャルの呼び出しを行う。

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

動作確認

追加したフィードが表示されていることを確認。

image

ただし、マイクロポストの投稿に失敗した場合は、エラーが発生する。 (@feed_itemsを期待しているが取得できていない)

image

対策として、DB保存失敗時には空の配列で@feed_itemsを定義する。

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end

DB保存失敗時にエラーが発生しないことを確認。

image

演習1

新しく実装したマイクロポストの投稿フォームを使って、実際にマイクロポストを投稿してみましょう。Railsサーバーのログ内にあるINSERT文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。

投稿フォームからマイクロポストを投稿する。

image

ログは以下の通り。 INSERT文もプリペアドステートメントになっている。

Started POST "/microposts" for 121.119.136.219 at 2017-08-19 05:37:49 +0000
Cannot render console from 121.119.136.219! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"qHHNxLqH2NJGPTtIdVD5Q4qaNFZXQVt9qkT0D3caNiy5wZ5QD4JaK/VGomjss64pq7nSXcneQXcPCehIT5W3rQ==", "micropost"=>{"content"=>"投稿テスト1行目\r\n投稿テスト2行目"}, "commit"=>"Post"}
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "投稿テスト1行目\r\n投稿テスト2行目"], ["user_id", 1], ["created_at", 2017-08-19 05:37:49 UTC], ["updated_at", 2017-08-19 05:37:49 UTC]]
   (10.9ms)  commit transaction
Redirected to https://rails-tutorial-yokoyan.c9users.io/
Completed 302 Found in 23ms (ActiveRecord: 11.9ms)

演習2

コンソールを開き、user変数にデータベース上の最初のユーザーを代入してみましょう。その後、Micropost.where(“user_id = ?”, user.id)とuser.microposts、そしてuser.feedをそれぞれ実行してみて、実行結果がすべて同じであることを確認してみてください。ヒント: ==で比較すると結果が同じかどうか簡単に判別できます。

実行結果は以下の通り。 すべて同じであることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails console --sandbox
Running via Spring preloader in process 6116
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>> 
?> user = User.first
  User Load (0.3ms)  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-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>
>> 
?> Micropost.where("user_id = ?", user.id) == user.microposts
  Micropost Load (0.6ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC  [["user_id", 1]]
  Micropost Load (0.5ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC
=> true
>> 
?> Micropost.where("user_id = ?", user.id) == user.feed
=> true

13.3.4 マイクロポストを削除する

本章での学び

本章では、マイクロポストの削除機能を実装する。

【view】マイクロポストのパーシャルのリファクタリング

削除リンクを追加する。 自分が投稿したマイクロポストのみ削除することができる。

<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>

【controller】Micropostsコントローラのdestroyアクションを実装する

request.referrerメソッドを使って、1つ前のページヘリダイレクトする。 リファラーが取得できない場合は、root_urlへリダイレクトする。

  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
  end```

####【controller】beforeフィルタを追加する
destroyアクション実施前実行するbeforeフィルタを追加する。
ログインしているユーザのマイクロポストが存在しない場合は、root_urlへリダイレクトする。

class MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] before_action :correct_user, only: [:destroy] ・・・略・・・ private

def micropost_params
  params.require(:micropost).permit(:content)
end

def correct_user
  @micropost = current_user.micropost.find_by(id: params[:id])
  redirect_to root_url if @micropost.nil?
end

end

###演習1
*マイクロポストを作成し、その後、作成したマイクロポストを削除してみましょう。次に、Railsサーバーのログを見てみて、DELETE文の内容を確認してみてください。*

ブラウザからアクセスし、任意のマイクロポストが削除できることを確認。

![image](https://qiita-image-store.s3.amazonaws.com/0/98130/8d412edb-66fc-6a56-2e93-c0c90528f74d.png)

ログは以下の通り。

Started DELETE “/microposts/302” for 121.119.136.219 at 2017-08-19 07:14:05 +0000 Cannot render console from 121.119.136.219! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by MicropostsController#destroy as HTML Parameters: {“authenticity_token”=>“IdXOzp0PdnF6givkKNnhrUQrfoTz5WNa3X9ZQHAbZU/nu0rzvvRjbr0Iix4fPzhqzmx435NnWbNhAk0Oz6TcOw==”, “id”=>“302”} User Load (0.2ms) SELECT “users”. FROM “users” WHERE “users”.“id” = ? LIMIT ? [[“id”, 1], [“LIMIT”, 1]] Micropost Load (0.2ms) SELECT “microposts”. FROM “microposts” WHERE “microposts”.“user_id” = ? AND “microposts”.“id” = ? ORDER BY “microposts”.“created_a ” DESC LIMIT ? [[“user_id”, 1], [“id”, 302], [“LIMIT”, 1]] (0.1ms) begin transaction SQL (108.7ms) DELETE FROM “microposts” WHERE “microposts”.“id” = ? “id”, 302 (17.0ms) commit transaction Redirected to https://rails-tutorial-yokoyan.c9users.io/ Completed 302 Found in 168ms (ActiveRecord: 128.5ms)

###演習2
*redirect_to request.referrer || root_urlの行をredirect_back(fallback_location: root_url)と置き換えてもうまく動くことを、ブラウザを使って確認してみましょう (このメソッドはRails 5から新たに導入されました)。*

確かにGithubでも、redirect_backメソッドを追加したって言ってます。
`redirect_to :back`だと、完全に安全ではなく、
リクエストで利用できるリファラーがない場合、`ActionController::RedirectBackError`が発生するとのこと。

[Add `redirect_back` and deprecate `redirect_to :back` #22506](https://github.com/rails/rails/pull/22506)

以下の記事もわかりやすかったです。

[Rails 5.1系からは redirect_to :backがきえまっせ](http://qiita.com/100010/items/3ae5dea25e85b0cbf1ee)

ということで、rediret_backメソッドを使うようにリファクタリング。

def destroy @micropost.destroy flash[:success] = “Micropost deleted” # redirect_to request.referrer || root_url redirect_back(fallback_location: root_url) end

問題なく動くことを確認。
新しいAPI使うほうがいいですね!

![image](https://qiita-image-store.s3.amazonaws.com/0/98130/9657403b-7b26-98f9-04fa-ab6b6cd5bdbc.png)


##13.3.5 フィード画面のマイクロポストをテストする
###本章での学び
作成したマイクロポストの単体テストと統合テストを作成する。

####【事前準備】テストデータの用意
マイクロポストのfixtureにデータを追加する。

・・・略・・・ ants: content: “Oh, is that what you want? Because that’s how you get ants!” created_at: <%= 2.years.ago %> user: archer

zone: content: “Danger zone!” created_at: <%= 3.days.ago %> user: archer

tone: content: “I’m sorry. Your words made sense, but your sarcastic tone did not.” created_at: <%= 10.minutes.ago %> user: lana

van: content: “Dude, this van’s, like, rolling probable cause.” created_at: <%= 4.hours.ago %> user: lana

####【test】テストコードの作成
マイクロポストのテストコードを作成する。
自分のマイクロポストではないものを削除して

- should redirect destroy for wrong micropost
    - michaelユーザでログインする
    - マイクロポストのfixtureのantsを取得する(archerユーザのマイクロポスト)
    - マイクロポストの件数が変わらないことを確認
        - マイクロポストを削除する
    - root_urlにリダイレクトすることを確認

test “should redirect destroy for wrong micropost” do log_in_as(users(:michael)) micropost = microposts(:ants) assert_no_difference ‘Micropost.count’ do delete micropost_path(micropost) end assert_redirected_to root_url end

実行結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test Running via Spring preloader in process 7833 Started with run options –seed 65115

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

Finished in 3.68433s 57 tests, 245 assertions, 0 failures, 0 errors, 0 skips

####【test】マイクロポストのUIに関する統合テストを作成する
rails generateで自動生成する。

yokoyan:~/workspace/sample_app (user-microposts) $ rails generate integration_test microposts_interface Running via Spring preloader in process 7975 Expected string default value for ‘–jbuilder’; got true (boolean) invoke test_unit create test/integration/microposts_interface_test.rb

- setup
    - michaelユーザの情報を取得する
    - @userに代入する

- micropost interface
    - ログインする
    - root_pathへ、getリクエストを送信する
    - HTMLに div class=pagination要素があることを確認
    - マイクロポストの数が変わらないことを確認
        - microposts_pathへpostリクエストを送信
            - params
                - micropost
                - content属性は空
    - HTMLにdiv id=error_explanation要素があることを確認
    - 変数contentに文字列を代入
    - マイクロポストの数が変わることを確認
        - microposts_pathへpostリクエストを送信
            - params
                - micropost
                - content属性は代入した変数content
    - root_urlへリダイレクトされること
    - リダイレクトを追う
    - 代入したcontentの文字列が、HTML内に一致すること
    - HTML内にdeleteテキストとaタグがあることを確認
    - ユーザの1番目のマイクロポストを取得する
    - マイクロポストの数が-1されることを確認
        - micropost_pathへdeleteリクエストを送信
    - archerユーザのプロフィールページヘgetリクエストを送信
    - HTML内にdeleteテキストとaタグが0件であることを確認

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

test “micropost interface” do log_in_as(@user) get root_path assert_select ‘div.pagination’ # 無効な送信 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” assert_difference ‘Micropost.count’, 1 do post microposts_path params: { micropost: { content: content } } end assert_redirected_to root_url follow_redirect! assert_match content, response.body # 投稿を削除する assert_select ‘a’, text: ‘delete’ micropost = @user.microposts.paginate(page: 1).first assert_difference ‘Micropost.count’, -1 do delete micropost_path(micropost) end # 違うユーザーのプロフィールにアクセス(削除リンクがないことを確認) get user_path(users(:archer)) assert_select ‘a’, text: ‘delete’, count: 0 end

実行結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test Running via Spring preloader in process 2271 Started with run options –seed 12967

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

Finished in 3.92997s 58 tests, 255 assertions, 0 failures, 0 errors, 0 skips

###演習1
*リスト 13.55で示した4つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが redになることを確認し、元に戻すと greenになることを確認してみましょう。*

####「無効な送信」のテスト
createメソッドをコメントアウトする。

# def create # @micropost = current_user.microposts.build(micropost_params) # if @micropost.save # flash[:success] = “Micropost created!” # redirect_to root_url # else # @feed_items = [] # render ‘static_pages/home’ # end # end

16行目でエラーになることを確認。

ERROR[“test_micropost_interface”, MicropostsInterfaceTest, 3.938271726015955] test_micropost_interface#MicropostsInterfaceTest (3.94s) AbstractController::ActionNotFound: AbstractController::ActionNotFound: The action ‘create’ could not be found for MicropostsController test/integration/microposts_interface_test.rb:17:in block (2 levels) in <class:MicropostsInterfaceTest>' test/integration/microposts_interface_test.rb:16:inblock in <class:MicropostsInterfaceTest>'

####「有効な送信」のテスト
DB保存に成功した際の処理をコメントアウトする。
@micropost = current_user.microposts.build(micropost_params)
if @micropost.save
  # flash[:success] = "Micropost created!"
  # redirect_to root_url
else
25行目でエラーになることを確認。

FAIL[“test_micropost_interface”, MicropostsInterfaceTest, 2.1839206719305366] test_micropost_interface#MicropostsInterfaceTest (2.18s) Expected response to be a <3XX: redirect>, but was a <204: No Content> test/integration/microposts_interface_test.rb:25:in `block in <class:MicropostsInterfaceTest>'

####「投稿を削除する」のテスト
deleteリンクを表示させないために、@feed_itemsの描画をコメントアウトする。

<% if @feed_items.any? %>

    –>

29行目でエラーになることを確認。

FAIL[“test_micropost_interface”, MicropostsInterfaceTest, 1.6857899979222566] test_micropost_interface#MicropostsInterfaceTest (1.69s) expected but was .. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:29:in `block in <class:MicropostsInterfaceTest>'

####「違うユーザーのプロフィールにアクセス」のテスト
ログインユーザーとマイクロポストのユーザーが一致するかの判定をコメントアウト。
<% #if current_user?(micropost.user) %>
  <%= link_to "delete", micropost, method: :delete,
                                    data: { confirm: "You sure?" } %>
<% #end %>
36行目でエラーになることを確認。

FAIL[“test_micropost_interface”, MicropostsInterfaceTest, 2.53247292409651] test_micropost_interface#MicropostsInterfaceTest (2.53s) Expected exactly 0 elements matching “a”, found 2.. Expected: 0 Actual: 2 test/integration/microposts_interface_test.rb:36:in `block in <class:MicropostsInterfaceTest>'


###演習2
*サイドバーにあるマイクロポストの合計投稿数をテストしてみましょう。このとき、単数形 (micropost) と複数形 (microposts) が正しく表示されているかどうかもテストしてください。ヒント: リスト 13.57を参考にしてみてください。*

- micropost sidebar count
    - michaelユーザでログイン
    - root_pathへgetリクエスト送信
    - michaelユーザのマイクロポスト数をカウントし、複数形で表示されることを確認
    - 別ユーザ(maloryユーザ)でログイン
    - root_pathへgetリクエスト送信
    - HTMLに"0 micropost"と単数形で表示されることを確認
    - maloryユーザでマイクロポストを投稿する
    - root_pathへgetリクエスト送信
    - HTMLに"1 micropost"と単数形で表示されることを確認

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

test “micropost sidebar count” do log_in_as(@user) get root_path assert_match “#{ @user.microposts.count } microposts”, response.body # まだマイクロポストを投稿していないユーザー other_user = users(:malory) log_in_as(other_user) get root_path assert_match “0 microposts”, response.body other_user.microposts.create!(content: “A micropost”) get root_path assert_match “1 micropost”, response.body end

#おわりに
マイクロポストの投稿から、フィード表示、削除までマイクロポストの主要機能が実装できました。
本章はボリュームがあってしんどかったですが、
ユーザーのCRUD処理と似ていたので、理解しやすかったです。

【13章】Railsチュートリアル中にcould not find expected ':' while scanning a simple keyが出た時の対処法

発生した章

13.2.3 プロフィール画面のマイクロポストをテストするにて、リスト 13.27: ユーザーと関連付けされたマイクロポストのfixtureの内容を修正したら、テストでエラーが出るようになった。

発生したエラー内容

ERROR["test_should_redirect_edit_when_not_logged_in", UsersControllerTest, 3.9885445390827954]
 test_should_redirect_edit_when_not_logged_in#UsersControllerTest (3.99s)
ActiveRecord::Fixture::FormatError:         ActiveRecord::Fixture::FormatError: a YAML error occurred parsing /home/ubuntu/workspace/sample_app/test/fixtures/microposts.yml. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html
        The exact error was:
          Psych::SyntaxError: (<unknown>): could not find expected ':' while scanning a simple key at line 24 column 1

原因

ymlファイル内の属性に:がないため。

今回の原因

micropost_<%= n %>に、:がついていない

<% 30.times do |n| %>
micropost_<%= n %>
  content: <%= Faker::Lorem.sentence(5) %>
  created_at: <%= 42.days.ago %>
  user: michael
<% end %>

修正方法

micropost_<%= n %>に、:をつける

<% 30.times do |n| %>
micropost_<%= n %>:

無事にエラーが解決しました。