0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.14 - TDDでPost機能をコーディング part3 -

Last updated at Posted at 2020-04-07

はじめに

こんにちは!
またまたPost機能の開発の続きです。今回でラスト!

前回のソースコード

前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

前回の残り

  1. 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  • 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  • サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  • サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  • サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
  • サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

残り6シナリオ。ユーザー詳細ページにそのユーザーのポストを表示する機能ですね。

では早速最後のコーディングをしていきましょう!!

未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること

ユーザー詳細ページでそのユーザーのポストが投稿日時降順で表示されていることと、他のユーザーのポストが表示されていないことを検証します。

spec/system/07_posts_spec.rb
  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

部分テンプレートは頭に_をつけるのが習わしです。

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からこの部分テンプレートを読み込んで今と変わらない状態になることを確認してみましょう。

app/views/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を渡してあげればいいことになります。

app/views/users/show.html.erb
  <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になりました!

未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

ポストページの場合はユーザー名クリックでユーザー詳細ページへ遷移させていましたが、ユーザー詳細ページ上では

spec/system/07_posts_spec.rb
  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と変わりないのですが、リンク先が今のパスの場合は単なるテキストを表示してくれるメソッドです。

app/views/posts/_posts_list.erb
- <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つのテストのサインイン済版ですね。

spec/system/07_posts_spec.rb
  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です。

サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

これも未サインインで同じテストをしているのでそれをパクります。

spec/system/07_posts_spec.rb
  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

これも実装済みなのでテストがパスしていますね。

サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること

これも以前のテストとほぼ同じ。

spec/system/07_posts_spec.rb
  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

これもパス。

サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

これも同じようなテストをすでにしていますね。

spec/system/07_posts_spec.rb
  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回に分けてアプリケーションをHerokuEKSにデプロイしてみようと思います。ここまでできれば、自分の好きなサービスを作って世に公開することができます。

ではまた次週!

後片付け

# exit
$ docker-compose down

本日のソースコード

Other Hands-on Links

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?