はじめに
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のフィードに含まれていること
- lanaのマイクロポストを取得
- 自分自身の投稿を確認
- michaelのマイクロポストを取得
- michaelのフィードに含まれていること
- michaelのマイクロポストを取得
- フォローしていないユーザの投稿を確認
- archerのマイクロポストを取得
- michaelのフィードに含まれていないこと
- archerのマイクロポストを取得
上記を踏まえて実装する。
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
ブラウザからもフォローしているユーザのフィードが表示されていることを確認。
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=\"✓\" />\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'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'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=\"#\">← 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 →</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さん、安川さんに感謝です!