はじめに
こんにちは!
今回は前回のPost機能のコーディングの続きです。ユーザー詳細ページでそのユーザーが投稿したポストに絞って表示させる機能をコーディングします。
前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
前回の残り
まずは前回やり残したテストシナリオをもう一度確認しておきましょう。
- ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
- ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
- ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
- サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
残り11シナリオ。今回は5シナリオをやっていきます!それでポストページが完了、あとはユーザー詳細ページにポストを表示するストーリーだけになります。
今回もコンテナを立ち上げてコンテナの中でコマンドを実行していきたいと思います。
$ docker-compose up -d
$ docker-compose exec web ash
ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
今回もテストコードからです。「ポストする」ボタンにはpost_button
のid
を付与するとしましょう。
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること" do
# テスト用のユーザーを作成する
+ user = create_user(1)
# このテストシナリオで使うポスト内容として空文字を定義する
+ content = ""
# このテストシナリオで期待するエラーメッセージを定義する
+ error_message = "ポストを入力してください"
# テスト開始前のDB内のPostの数を記憶しておく
+ post_count = Post.count
# userでサインインする
+ sign_in(user)
+
# ポストページにアクセスする
+ visit posts_path
# ポスト入力欄にcontentを入力する
+ fill_in :post_content, with: content
# 投稿するボタン(#post_button)をクリックする
+ click_on :post_button
+
# 現在のページがポストページであることを検証する
+ expect(current_path).to eq posts_path
# ページ内に期待するエラーメッセージが表示されていることを検証する
+ expect(page).to have_text error_message
# DB内のPostの数が変わらない(=Postの登録が失敗している)ことを検証する
+ expect(Post.count).to eq post_count
+ end
end
検証内容としては特に「期待するエラーメッセージが表示されていること」と「DB内のPostの数に変化がないこと」を検証することでバリデーションが効いていることを確認しています。
では、テストを実行してみましょう。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい After sign in ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
Failure/Error: click_on :post_button
Capybara::ElementNotFound:
Unable to find link or button :post_button
Finished in 20.3 seconds (files took 7.34 seconds to load)
11 examples, 1 failure
#post_button
がないので怒られました。ので、ボタンを追加しましょう。
<div class="container my-5">
<%= form_with model: @post, url: nil, local: true do |form| %>
<div class="form-group">
<%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %>
</div>
+ <div class="text-right">
+ <%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %>
+ </div>
<% end %>
</div>
post_button
を追加してみました。UI的には以下のような感じになっているはず!
OK。ボタンは探せるようになったはずなのでまたテスト。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
Failure/Error: expect(page).to have_text error_message
expected to find text "ポストを入力してください" in "The page you were looking for doesn't exist.\nYou may have mistyped the address or the page may have moved.\nIf you are the application owner check the logs for more information."
Finished in 11.97 seconds (files took 8.31 seconds to load)
11 examples, 1 failure
次はエラーメッセージがみつからないみたいです。確かにエラーメッセージを表示する機能は作っていない!
まずエラーメッセージを表示するためには、ポストを送信してバリデーションに引っかかり、そのエラーメッセージをポストページ、つまりposts/index.html.erb
で表示できるようにする必要があります。
まずは、Postsコントローラーにcreate
アクションを作成して、そのアクションにポストを送信できるようにルーティングとform_with
を更新しましょう。
class PostsController < ApplicationController
def index
redirect_to root_path unless signed_in?
@post = Post.new
end
+
+ def create
+ # 未サインインの場合、トップページにリダイレクトする
+ redirect_to root_path unless signed_in?
+ # Strong parameterからリクエスト内容の通りにPostモデルオブジェクトを作成する
+ @post = Post.new(post_params)
+ # Postモデルオブジェクトのuser_idにサインイン中のuserのidを定義する
+ @post.user = current_user
+
+ if @post.save
+ # Todo: DB保存が成功した場合の動作
+ else
+ # DB保存が失敗した場合、ポストページをレンダリングする
+ render :index
+ end
+ end
+
+ private
+ # PostのStrong parameter
+ def post_params
+ params.require(:post).permit(:content)
+ end
end
@post.user
はあえてフォームからの送信ではなく、サーバーサイドでcurrent_user
、つまりサインイン中のユーザーを設定しています。フォームから送信されたユーザーのIDを設定する方法も考えられますが、フォームの内容はユーザー側で簡単に改変できてしまうので違うユーザーのポストとして登録されてしまう危険性があります。
そのため、今回はサーバー側で処理をするようにしてみました。
Rails.application.routes.draw do
...
get '/posts', to: 'posts#index', as: :posts
+ post '/posts', to: 'posts#create', as: :create_post
end
...
- <%= form_with model: @post, url: nil, local: true do |form| %>
+ <%= form_with model: @post, url: create_post_path, local: true do |form| %>
...
これでcreate
アクションで@post.save
が失敗した場合、@post
にエラーメッセージが格納されてindex.html.erb
がレンダリングされるようになるはずです。
次に、@post.save
がcontent
が未入力の場合にエラーになるようにPost
モデルのcontent
属性にpresence
を定義しましょう。
class Post < ApplicationRecord
belongs_to :user
+
+ validates :content,
+ presence: true
end
Postモデルにはpresence
のバリデーションを定義します。
最後にエラーメッセージを表示できるようにViewを更新します。
<div class="container my-5">
+
+ <% if @post.errors.any? %>
+ <div class="alert alert-danger">
+ <ul class="mb-0">
+ <% @post.errors.full_messages.each do |msg| %>
+ <li><%= msg %></li>
+ <% end %>
+ </ul>
+ </div>
+ <% end %>
+
<%= form_with model: @post, url: create_post_path, local: true do |form| %>
<div class="form-group">
<%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %>
</div>
<div class="text-right">
<%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %>
</div>
<% end %>
</div>
これでポストが未入力の場合にエラーメッセージが表示されるようになっているはずです。再びテストを実行してみます。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
Failure/Error: expect(page).to have_text error_message
expected to find text "ポストを入力してください" in "sample app\nPosts\nProfile\nSign out\nContentを入力してください\n(c) Hoge Inc. All Rights Reserved."
Finished in 19.22 seconds (files took 7.93 seconds to load)
11 examples, 1 failure
またテスト失敗理由が変わりました。
今度は「ポストを入力してください」というエラーメッセージを期待していたが、「Contentを入力してください」となってしまっていたようです。
属性の日本語表記はconfig/locales/ja.yml
で定義していますので、更新していきます。
ja:
activerecord:
attributes:
user:
name: "お名前"
email: "メールアドレス"
password: "パスワード"
password_confirmation: "確認用パスワード"
+ post:
+ content: "ポスト"
errors:
...
ここまででモデルのバリデーションとエラーメッセージの表示までの実装を終えました。ではテストを実行してみましょう。
# rspec spec/system/07_posts_spec.rb
Finished in 19.99 seconds (files took 7.33 seconds to load)
11 examples, 0 failures
これでテストがGreenの状態になりました。
ここで少しリファクタリングしておきます。今、Posts
コントローラーのindex
アクションとcreate
アクションの最初に未サインインであればトップページにリダイレクトする、全く同じ処理を書いてしまっています。
これではDRYではないので、メソッド化してbefore_action
で実行するようにリファクタします。
class PostsController < ApplicationController
+ before_action :redirect_to_root_unless_signed_in
def index
- redirect_to root_path unless signed_in?
@post = Post.new
end
def create
- redirect_to root_path unless signed_in?
@post = Post.new(post_params)
@post.user = current_user
if @post.save
else
render :index
end
end
private
def post_params
params.require(:post).permit(:content)
end
+
+ # 未サインインの場合、トップページにリダイレクトするメソッド
+ def redirect_to_root_unless_signed_in
+ redirect_to root_path unless signed_in?
+ end
end
はい。before_action
にひとまとめにしてみました。
ではこれでもちゃんとテストがパスするか確認をしておきましょう。
# rspec spec/system/07_posts_spec.rb
Finished in 18.5 seconds (files took 5.6 seconds to load)
11 examples, 0 failures
Greenな状態をキープしたままリファクタリングができましたね!
ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
まずはテストです!
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること" do
+ # テスト用のユーザーを作成する
+ user = create_user(1)
+ # このテストシナリオで使うポスト内容として141文字を定義する
+ content = "a" * 141
+ # このテストシナリオで期待するエラーメッセージを定義する
+ error_message = "ポストは140文字以内で入力してください"
+ # テスト開始前のDB内のPostの数を記憶しておく
+ post_count = Post.count
+ # userでサインインする
+ sign_in(user)
+
+ # ポストページにアクセスする
+ visit posts_path
+ # ポスト入力欄にcontentを入力する
+ fill_in :post_content, with: content
+ # 投稿するボタン(#post_button)をクリックする
+ click_on :post_button
+
+ # 現在のページがポストページであることを検証する
+ expect(current_path).to eq posts_path
+ # ページ内に期待するエラーメッセージが表示されていることを検証する
+ expect(page).to have_text error_message
+ # ポスト入力欄に入力していたポスト内容がそのまま残っていることを検証する
+ expect(find("#post_content").value).to eq content
+ # DB内のPostの数が変わらない(=Postの登録が失敗している)ことを検証する
+ expect(Post.count).to eq post_count
+ end
end
テスト実行です。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
Failure/Error: expect(page).to have_text error_message
expected to find text "ポストは140文字以内で入力してください" in "sample app\nPosts\nProfile\nSign out\n(c) Hoge Inc. All Rights Reserved."
Finished in 27.16 seconds (files took 5.71 seconds to load)
12 examples, 1 failure
エラーメッセージを見つけられないため失敗しているようです。今、140文字の制限をPost
モデルのcontent
属性に付与していないのでPost.save
がtrue
だったのでしょう。
バリデーションを追加してみます。
class Post < ApplicationRecord
belongs_to :user
validates :content,
- presence: true
+ presence: true,
+ length: { maximum: 140 }
end
では、またテストを実行してみます。
# rspec spec/system/07_posts_spec.rb
Finished in 22.78 seconds (files took 6.21 seconds to load)
12 examples, 0 failures
Greenになりました!
ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
テストから。
...
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること" do
+ # テスト用のユーザーを作成する
+ user = create_user(1)
+ # このテストシナリオで使うポスト内容を4つ用意する
+ # "Hello, world.": 通常のポスト内容
+ # "a": 0文字がNGなので境界値として1文字のポスト内容を用意
+ # "a" * 140: 141文字がNGなので境界値として140文字のポスト内容を用意
+ # "Hello.\nWorld.": 特殊なケースとして改行が入っているポスト内容を用意
+ contents = ["Hello, world.", "a", "a" * 140, "Hello.\nWorld."]
+ # userでサインインする
+ sign_in(user)
+
+ # contentsの中から1つずつをテストする
+ contents.each do |content|
+ # テスト開始前のDB内のPostの数を記憶しておく
+ post_count = Post.count
+
+ # ポストページにアクセスする
+ visit posts_path
+ # ポスト入力欄にcontentを入力する
+ fill_in :post_content, with: content
+ # 投稿するボタン(#post_button)をクリックする
+ click_on :post_button
+
+ # 現在のページがポストページであることを検証する
+ expect(current_path).to eq posts_path
+ # ポスト内容がクリアされていることを検証する
+ expect(find("#post_content").value).to eq ""
+ # DB内のPostが1つ増えている(=投稿したポストが保存された)ことを検証する
+ expect(Post.count).to eq post_count + 1
+ # ポストページのポスト一覧の一番上に投稿したポストが表示されていることを検証する
+ expect(find("#posts_list").all(".post-item").first).to have_text content
+ expect(find("#posts_list").all(".post-item").first).to have_text user.name
+ end
+ end
end
あまりeach
とかを使うとどのケースでテスト失敗したのか行数からはわかりにくくなってしまう危険性もあるのですが、長く書くのも面倒なので今回はいくつかの文字列をポストするテストをeach
で繰り返してみました。
"a"
と"a" * 140
は境界値のテストをしています。
"Hello.\nWorld."
は改行が入った時に正しく登録・表示されるかをテストするためのパターンです。
posts_list
はポストページで過去のポストが一覧表示されるエリアのid
としてます。その中で各ポストにはpost-item
のclass
を割り当ててall(".post-item")
でそのコレクションを取得できるようにしようと思います。.first
でその中でも一番最初に表示されるもの、つまり一番上に表示される要素を検証対象としています。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
Failure/Error: expect(find("#post_content").value).to eq ""
expected: ""
got: "Hello, world."
(compared using ==)
Finished in 22.11 seconds (files took 6.73 seconds to load)
13 examples, 1 failure
テストは失敗しています。
ポストが成功した場合はポストの入力エリアが空白に戻るようにしたいのですがそれができていないようです。
ポスト成功時の動作をposts#create
でまだコーディングできていないので記述していきます。
単にposts#index
アクションにリダイレクトしてあげるだけで、index
アクションの中で@post = Post.new
が実行されるのでcontent
を初期化できそうです。
class PostsController < ApplicationController
...
def create
@post = Post.new(post_params)
@post.user = current_user
if @post.save
+ redirect_to posts_path
else
render :index
end
end
a...
end
これで完成。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content
Capybara::ElementNotFound:
Unable to find css "#posts_list"
Finished in 26.85 seconds (files took 7.87 seconds to load)
13 examples, 1 failure
失敗理由が変わりました。#posts_list
なんて要素ないよ、とのことなのでViewを作っていきましょう。
まずは#posts_list
で表示するデータをコントローラー側で取得しておく必要がありますので、posts#index
アクションで全てのPostを作成日降順で取得するようにしましょう。
class PostsController < ApplicationController
...
def index
@post = Post.new
+ # 更新日時降順で全てのポストを@postsに代入する
+ @posts = Post.order(created_at: :desc)
end
...
end
全てのポストを作成日時降順で@posts
に入れてます。
続いてはこのインスタンス変数を使ってViewを作っていきます。
<div class="container my-5">
<% if @post.errors.any? %>
<div class="alert alert-danger">
<ul class="mb-0">
<% @post.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form_with model: @post, url: create_post_path, local: true do |form| %>
<div class="form-group">
<%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %>
</div>
<div class="text-right">
<%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %>
</div>
<% end %>
+
+ <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"><%= post.user.name %></h5>
+ <p class="card-text"><%= post.content %></p>
+ </div>
+ </div>
+ <% end %>
+ </div>
</div>
form_with
よりも下にid=posts_list
のフィールドを作って@posts
をひとつずつ表示してます。
post.user.name
でpost
の外部キーuser_id
のUserモデルオブジェクトのname
を取得して表示してます。PostモデルとUserモデルを関連付けしているのでこういった使い方ができるんですね。
では、テストしてみましょう。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい After sign in ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
Failure/Error: expect(page).to have_text error_message
expected to find text "ポストを入力してください" in "We're sorry, but something went wrong.\nIf you are the application owner check the logs for more information."
2) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
Failure/Error: expect(page).to have_text error_message
expected to find text "ポストは140文字以内で入力してください" in "We're sorry, but something went wrong.\nIf you are the application owner check the logs for more information."
3) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content
expected to find text "Hello.\nWorld." in "John Smith\nHello. World."
Finished in 34 seconds (files took 5.62 seconds to load)
13 examples, 3 failures
うぉ、3つもテストが失敗している...
1)
と2)
はなにやら例外が発生してしまっているようです。試しにdevelopment
環境でポストを未入力で「ポストする」ボタンをクリックしてみましょう。
ふむふむ。<% @posts.each do |post| %>
のところでundefined method 'each'
が起きてますね。
この2つのテストはposts#create
アクションで@post.save
がfalse
の時にrender :index
でレンダリングさせているケースです。
もう一度コードをよくみると、このレンダリングまでにこのアクションでは@posts
というインスタンス変数を定義していません。
今回の例外は@posts
というモデルオブジェクトのインスタンス変数がないにもかかわらず@posts.each
を使おうとしていることから起きた例外と推測できます。
posts#create
アクションを見直してみましょう。
class PostsController < ApplicationController
...
def create
@post = Post.new(post_params)
@post.user = current_user
if @post.save
redirect_to posts_path
else
+ # 更新日時降順で全てのポストを@postsに代入する
+ @posts = Post.order(created_at: :desc)
render :index
end
end
...
end
@post.save
がfalse
の場合、render
の前に@posts
を定義するようにしてみました。
例外が解消されたかもう一度テストしてみましょう。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content
expected to find text "Hello.\nWorld." in "John Smith\nHello. World."
Finished in 32.49 seconds (files took 7.4 seconds to load)
13 examples, 1 failure
ふー。今取り組んでいるテストシナリオだけがテスト失敗している状態に戻せましたね。
内容をみてみると"Hello.\nWorld."
のポストが成功しているはずなのにページに表示されていないようです。
スクリーンショットをみると、確かに一番上のポストの文字列が改行されていないHello. World.
という文字列になっていますね。
実はpost.content
のような書き方では文字列を表示することはできるのですが、改行がうまく表現できません。
これを解決するために、今回はsafe_join
メソッドを使って改行を正しく表示できるようにします。(参考: 「simple_format」や「safe_join」を使って、正常に改行表示させる方法(Rails) - りょうたくの技術ブログ)
- <p class="card-text"><%= post.content %></p>
+ <p class="card-text"><%= safe_join(post.content.split("\n"), tag(:br)) %></p>
ちょっと書き方が複雑ですが、これでテキストエリアでつけた改行の通りに改行を表現することができるようになります。
こういったやり方はいろいろあるので、どう表現したいかによって使い分けが必要です。
では再度テストを実行してみましょう。
# rspec spec/system/07_posts_spec.rb
Finished in 30.57 seconds (files took 7.13 seconds to load)
13 examples, 0 failures
無事テストをパスすることができました!!
このように、テストで用いるデータの種類をいろいろなケース用意することで本当に期待通りの動作をしているかを正しく把握し実装することができるのです。
サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
ここからはポストページの表示系ですね。
まずは投稿日時降順で表示ができているかという点です。複数ユーザーが投稿していることにします。
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること" do
+ # テスト用のユーザーを作成する
+ user1 = create_user(1)
+ user2 = create_user(2)
+ # ポストを用意する
+ posts = []
+ posts.unshift Post.create(content: "first post", user: user1)
+ posts.unshift Post.create(content: "初めてのポスト", user: user2)
+ posts.unshift Post.create(content: "second post!!", user: user1)
+ # userでサインインする
+ sign_in(user1)
+
+ # ポストページにアクセスする
+ visit posts_path
+
+ # 投稿日時降順でポストが表示されていることを検証する
+ posts.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
+ end
end
今回は適当に3つのポストを生成しておき、作成日時降順でポストが表示されているかをチェックしています。
posts
という空のArrayを最初に作成し、unshift
メソッドを使って配列の先頭にPost.create
を挿入していきます。最終的には作成日時降順で配列にPostのモデルオブジェクトが格納されるかたちになります。(参考: Rubyで配列に要素を追加・挿入する:push, insert, unshift | UX MILK)
これをeach_with_index
でモデルオブジェクトとindexを取り出して、posts_list
のi
番目にある要素が新しい方からi
番目に作成されたポストであるかどうかを検証しているというわけです。(参考: Rubyのeachでindexを取得する:each_with_index | UX MILK)
では、テストを実行してみましょう。
# rpsec spec/system/07_posts_spec.rb
Finished in 30.64 seconds (files took 6.38 seconds to load)
14 examples, 0 failures
この辺りはすでに作り込みが終わっているのでテストがパスしていますね。
サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
まずはテストです。ポストしたユーザーの名前を表示している要素にはpost-user-name
というclass
をつけることにします。
feature "ユーザーとして、ポストを投稿したい", type: :system do
...
+ scenario "サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do
+ # テスト用のユーザーを作成する
+ user1 = create_user(1)
+ user2 = create_user(2)
+ # ポストを用意する
+ posts = []
+ posts.unshift Post.create(content: "First Post!!", user: user1)
+ posts.unshift Post.create(content: "初めてのポスト", user: user2)
+ # user1でサインインする
+ sign_in(user1)
+
+ posts.each_with_index do |post, i|
+ # ポストページにアクセスする
+ visit posts_path
+ # 上からi番目のポストのユーザー名をクリックする
+ find("#posts_list").all(".post-item")[i].find(".post-user-name").click
+
+ # 現在のページがクリックしたポストのユーザーのユーザー詳細ページであることを検証する
+ expect(current_path).to eq user_path(post.user)
+ end
+ end
end
いままではclick_on
を使ってクリック操作を実装していましたが、要素.click
でも同じようにクリック操作ができます。click_on
の場合はボタンやリンクに限定されていたのですが、click
の場合はどんな要素であれクリック操作ができるので必要になるケースもあります。覚えておいてくださいね。
# rspec spec/system/07_posts_spec.rb
Failures:
1) ユーザーとして、ポストを投稿したい After sign in Created posts サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
Failure/Error: find("#posts_list").all(".post-item")[i].find(".post-user-name").click
Capybara::ElementNotFound:
Unable to find css ".post-user-name" within #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[1]/DIV[1]/DIV[1]">
Finished in 35.66 seconds (files took 6.04 seconds to load)
15 examples, 1 failure
post-user-name
classの要素が見つからないようですね。現在ポストしたユーザーの名前を表示している要素にpost-user-name
classのリンクを定義しましょう。
- <h5 class="card-title post-user-name"><%= post.user.name %></h5>
+ <h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>
link_to
メソッドを使ってリンクを生成してみました。
# rspec spec/system/07_posts_spec.rb
Finished in 34.14 seconds (files took 5.9 seconds to load)
15 examples, 0 failures
テストパス!
まとめ
はい。今日はここまでです!
今回まででポストページの機能を実装することができましたね。
あとはユーザー詳細ページ側でそのユーザーのポストを表示するテストと機能を実装していきましょう!
ではまた次週!
後片付け
# exit
$ docker-compose down