はじめに
こんにちは!
またまたPost機能の開発の続きです。今回でラスト!
前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
前回の残り
- 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
残り6シナリオ。ユーザー詳細ページにそのユーザーのポストを表示する機能ですね。
では早速最後のコーディングをしていきましょう!!
未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
ユーザー詳細ページでそのユーザーのポストが投稿日時降順で表示されていることと、他のユーザーのポストが表示されていないことを検証します。
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること" do
+ # テスト用のユーザーを作成する
+ user1 = create_user(1)
+ user2 = create_user(2)
+ # ポストを用意する
+ posts1 = []
+ posts1.unshift Post.create(content: "First Post!!", user: user1)
+ posts1.unshift Post.create(content: "Second Post!!", user: user1)
+ posts2 = []
+ posts2.unshift Post.create(content: "初めてのポスト", user: user2)
+ posts2.unshift Post.create(content: "2回目のポスト", user: user2)
+
+ # user1のユーザー詳細ページにアクセスする
+ visit user_path(user1)
+
+ # user1のポストが投稿日時降順で表示されていることを検証する
+ posts1.each_with_index do |post, i|
+ expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+ expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+ end
+ # user2のポストは表示されないことを検証する
+ posts2.each do |post|
+ expect(page).not_to have_text post.user.name
+ expect(page).not_to have_text post.content
+ end
+
+ # user2のユーザー詳細ページにアクセスする
+ visit user_path(user2)
+
+ # user2のポストが投稿日時降順で表示されていることを検証する
+ posts2.each_with_index do |post, i|
+ expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+ expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+ end
+ # user1のポストは表示されないことを検証する
+ posts1.each do |post|
+ expect(page).not_to have_text post.user.name
+ expect(page).not_to have_text post.content
+ end
+ end
end
少し長いですが、今までの延長で理解できるコードになっているはずです!(コメントアウトも参考にしてね。)
さて、このテストを回してみましょう。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
Failure/Error: expect(find("#posts_list").all(".post-item")[i].find(".post-user-name")).to have_text user.name
Capybara::ElementNotFound:
Unable to find css "#posts_list"
Finished in 27.77 seconds (files took 5.67 seconds to load)
16 examples, 1 failure
この時点では#posts_list
がないと怒られます。posts_list
、つまりユーザー詳細ページでポストを表示する機能をコーディングしていないので、テストが失敗しています。
さて、今回のposts_list
ですが、ポストページで同じようにポストの一覧を表示するViewを作りました。開発を効率的に進めるために、是非その時の機能を利用したいですね。
Railsでは部分テンプレート(Partial Template)という機能があります。複数のテンプレートから呼び出されるような一部分のViewを別ファイルに切り出して、各テンプレートからそれを呼び出すようなイメージです。
百聞は一見にしかずですので、まずは試してみましょう。
まずは、ポストページのposts_list
配下の要素を部分テンプレート化してみます。
# touch app/views/posts/_posts_list.html.erb
部分テンプレートは頭に_
をつけるのが習わしです。
<% posts.each do |post| %>
<div class="card post-item my-1">
<div class="card-body">
<h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>
<p class="card-text"><%= safe_join(post.content.split("\n"), tag(:br)) %></p>
</div>
</div>
<% end %>
部分テンプレートはこんな感じで書きます。posts/index.html.erb
に書いていた内容と変わらないです。唯一変わるポイントは最初のeach
する配列の変数が@posts
からposts
になっていることです。
部分テンプレートは別のテンプレートファイルから呼び出されますが、その時にインスタンス変数でなくても変数を渡すことができます。これも実際にみてみた方が早いと思いますので、まずはposts/index.html.erb
からこの部分テンプレートを読み込んで今と変わらない状態になることを確認してみましょう。
...
<div id="posts_list" class="my-5">
- <% @posts.each do |post| %>
- <div class="card post-item my-1">
- <div class="card-body">
- <h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>
- <p class="card-text"><%= safe_join(post.content.split("\n"), tag(:br)) %></p>
- </div>
- </div>
- <% end %>
+ <%= render partial: "posts_list", locals: { posts: @posts } %>
</div>
...
呼び出し方はrender partial:
に対して適用したいテンプレートのファイル名(頭の_
は除く)を指定するだけです。さらにオプションでlocals:
の後に{ 変数名: 値 }
をつけることで部分テンプレートに変数を受け渡すことができます。今回の例では@posts
を部分テンプレート内のposts
に代入させていることになります。
変数は複数受け渡すことができ、その場合は{ 変数名1: 値1, 変数名2: 値2 }
のように,
で区切るだけです。
ここで一度デグレが起きていないかテストを実行しておきましょう。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
Failure/Error: expect(find("#posts_list").all(".post-item")[i].find(".post-user-name")).to have_text user.name
Capybara::ElementNotFound:
Unable to find css "#posts_list"
Finished in 37.59 seconds (files took 6.58 seconds to load)
16 examples, 1 failure
今取り掛かっているシナリオのテスト失敗だけなので、ポストページに関するテスト失敗は起きていませんね。うまく部分テンプレートが機能しているようです。
さて、ユーザー詳細ページでもこの部分テンプレートを利用しましょう。
ユーザー詳細ページではそのユーザーのポストだけを表示したいので、部分テンプレートに渡すposts
変数にはそのユーザーのポストのArrayを渡してあげればいいことになります。
<div class="container my-5">
<% flash.each do |msg_type, msg| %>
<div class="alert alert-<%= msg_type %>"><%= msg %></div>
<% end %>
<%= @user.name %>
<br>
<%= @user.email %>
+
+ <div id="posts_list" class="my-5">
+ <%= render partial: "posts/posts_list", locals: { posts: @user.posts.order(created_at: :desc) } %>
+ </div>
</div>
先ほどと少し違うのは、_posts_list.html.erb
がこのファイルとは別のディレクトリ(posts/
)にあるので、そのディレクトリも含めて部分テンプレートファイル名を指定しています。(posts/posts_list
)
また、posts
変数には@user.posts.order(created_at: :desc)
でそのユーザーのポストを作成日時降順で取得したArrayを部分テンプレートに渡しています。
ではテストを回してみましょう。
# rspec spec/system/07_posts_spec.rb
Finished in 50.91 seconds (files took 7.01 seconds to load)
16 examples, 0 failures
Greenになりました!
未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
ポストページの場合はユーザー名クリックでユーザー詳細ページへ遷移させていましたが、ユーザー詳細ページ上では
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと" do
+ # テスト用のユーザーを作成する
+ user = create_user(1)
+ # テスト用のポストを作成する
+ posts = []
+ posts.unshift Post.create(content: "First Post!!", user: user)
+ posts.unshift Post.create(content: "Second Post!!", user: user)
+
+ # userのユーザー詳細ページにアクセスする
+ visit user_path(user)
+
+ # ポストのユーザー名がリンクになっていないことを検証する
+ posts.each_with_index do |post, i|
+ expect(find("#posts_list").all(".post-item")[i]).not_to have_selector("a.post-user-name")
+ end
+ end
end
ポストページと同じ部分テンプレートを使っているので、現在はポストのカードの中のユーザーの名前が表示されている要素はpost-user-name
をclass属性に付与されているa
タグになっています。
これがリンクを作っているところなので、この要素がない状態であれば、ユーザー名をクリックしても何も起こらないことを検証できます。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
Failure/Error: expect(find("#posts_list")).not_to have_selector("a.post-user-name")
expected not to find visible css "a.post-user-name" within #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[1]/DIV[1]">, found 2 matches: "John Smith", "John Smith"
Finished in 41.18 seconds (files took 5.05 seconds to load)
17 examples, 1 failure
現在はリンクがついてしまっているのでこれをなんとかします。
今link_to
を使ってリンクを作っていますが、link_to_unless_current
を使ってみます。使い方はlink_to
と変わりないのですが、リンク先が今のパスの場合は単なるテキストを表示してくれるメソッドです。
- <h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>
+ <h5 class="card-title"><%= link_to_unless_current post.user.name, post.user, class: "post-user-name" %></h5>
これでテストを実行してみましょう。
# rspec spec/system/07_posts_spec.rb
Finished in 34.46 seconds (files took 4.86 seconds to load)
17 examples, 0 failures
無事テストがパスしました。ポストページの方もデグレは起きていないかも全てのテストをパスしていることから確認できますね。
サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
上の2つのテストのサインイン済版ですね。
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること" do
+ # テスト用のユーザーを作成する
+ user1 = create_user(1)
+ user2 = create_user(2)
+ # ポストを用意する
+ posts1 = []
+ posts1.unshift Post.create(content: "First Post!!", user: user1)
+ posts1.unshift Post.create(content: "Second Post!!", user: user1)
+ posts2 = []
+ posts2.unshift Post.create(content: "初めてのポスト", user: user2)
+ posts2.unshift Post.create(content: "2回目のポスト", user: user2)
+ # user1でサインインする
+ sign_in(user1)
+
+ # user2のユーザー詳細ページにアクセスする
+ visit user_path(user2)
+
+ # user2のポストが投稿日時降順で表示されていることを検証する
+ posts2.each_with_index do |post, i|
+ expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+ expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+ end
+ # user1のポストは表示されないことを検証する
+ posts1.each do |post|
+ expect(page).not_to have_text post.user.name
+ expect(page).not_to have_text post.content
+ end
+ end
end
未サインインの時と検証内容は同じですね。
# rspec spec/system/07_posts_spec.rb
Finished in 36.67 seconds (files took 7.58 seconds to load)
18 examples, 0 failures
すでに実装済みですのでテストはGreenです。
サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
これも未サインインで同じテストをしているのでそれをパクります。
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと" do
+ # テスト用のユーザーを作成する
+ user1 = create_user(1)
+ user2 = create_user(2)
+ # テスト用のポストを作成する
+ posts = []
+ posts.unshift Post.create(content: "First Post!!", user: user2)
+ posts.unshift Post.create(content: "Second Post!!", user: user2)
+ # user1でサインインする
+ sign_in(user1)
+
+ # user2のユーザー詳細ページにアクセスする
+ visit user_path(user2)
+
+ # ポストのユーザー名がリンクになっていないことを検証する
+ posts.each_with_index do |post, i|
+ expect(find("#posts_list").all(".post-item")[i]).not_to have_selector("a.post-user-name")
+ end
+ end
end
# rspec spec/system/07_posts_spec.rb
Finished in 41.16 seconds (files took 5.61 seconds to load)
19 examples, 0 failures
これも実装済みなのでテストがパスしていますね。
サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
これも以前のテストとほぼ同じ。
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること" do
+ # テスト用のユーザーを作成する
+ user1 = create_user(1)
+ user2 = create_user(2)
+ # ポストを用意する
+ posts1 = []
+ posts1.unshift Post.create(content: "First Post!!", user: user1)
+ posts1.unshift Post.create(content: "Second Post!!", user: user1)
+ posts2 = []
+ posts2.unshift Post.create(content: "初めてのポスト", user: user2)
+ posts2.unshift Post.create(content: "2回目のポスト", user: user2)
+ # user1でサインインする
+ sign_in(user1)
+
+ # user1のプロフィールページにアクセスする
+ visit user_path(user1)
+
+ # user1のポストが投稿日時降順で表示されていることを検証する
+ posts1.each_with_index do |post, i|
+ expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+ expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+ end
+ # user2のポストは表示されないことを検証する
+ posts2.each do |post|
+ expect(page).not_to have_text post.user.name
+ expect(page).not_to have_text post.content
+ end
+ end
end
ほい。ではテストを回しましょう。
# rspec spec/system/07_posts_spec.rb
Finished in 41.83 seconds (files took 6.77 seconds to load)
20 examples, 0 failures
これもパス。
サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
これも同じようなテストをすでにしていますね。
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと" do
+ # テスト用のユーザーを作成する
+ user = create_user(1)
+ # テスト用のポストを作成する
+ posts = []
+ posts.unshift Post.create(content: "First Post!!", user: user)
+ posts.unshift Post.create(content: "Second Post!!", user: user)
+ # user1でサインインする
+ sign_in(user)
+
+ # user1のプロフィールページにアクセスする
+ visit user_path(user)
+
+ # ポストのユーザー名がリンクになっていないことを検証する
+ posts.each_with_index do |post, i|
+ expect(find("#posts_list").all(".post-item")[i]).not_to have_selector("a.post-user-name")
+ end
+ end
end
これもパスするはず。
# rspec spec/system/07_posts_spec.rb
Finished in 43.64 seconds (files took 6.54 seconds to load)
21 examples, 0 failures
パスしましたね。
ここまででポスト機能で定義したテストシナリオは全てパスできるアプリケーションを作ることができました!
最後に、今までのテストシナリオも含めてデグレの確認をしておきましょう!
# rspec
Finished in 1 minute 56.88 seconds (files took 6.09 seconds to load)
91 examples, 0 failures
2分ほど時間がかかりましたが、全てのテストをクリアできていました!!
まとめ
今日はここまでです!前回、前々回と3回に渡ってポスト機能をTDDでコーディングしてきましたがいかがだったでしょうか?
モデルの関連付け(has_many
, belongs_to
)や部分テンプレート(Partial Template)など新しく使ったものもありましたが、基本的な部分はハードルなくコーディングできるようになったのではないでしょうか?
実際にサービスをリリースするとなると、例えばアイコン登録とか、フォロー機能とか、いいね機能とか、、、作りたい機能がどんどんでてくるとは思いますが、すでに自分で調べながらコーディングをしていくことに対するハードルはなくなったんじゃないでしょうか?
ということでこのハンズオンのコーディング部分はこれで以上にしたいと思います。
最後はデプロイです!次とその次、2回に分けてアプリケーションをHeroku
とEKS
にデプロイしてみようと思います。ここまでできれば、自分の好きなサービスを作って世に公開することができます。
ではまた次週!
後片付け
# exit
$ docker-compose down