LoginSignup
0
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-31

はじめに

こんにちは!
今回は前回のPost機能のコーディングの続きです。ユーザー詳細ページでそのユーザーが投稿したポストに絞って表示させる機能をコーディングします。

前回のソースコード

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

前回の残り

まずは前回やり残したテストシナリオをもう一度確認しておきましょう。

  1. ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
  2. ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
  3. ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
  4. サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
  5. サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
  6. 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  7. 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  8. サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  9. サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  10. サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
  11. サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

残り11シナリオ。今回は5シナリオをやっていきます!それでポストページが完了、あとはユーザー詳細ページにポストを表示するストーリーだけになります。

今回もコンテナを立ち上げてコンテナの中でコマンドを実行していきたいと思います。

$ docker-compose up -d
$ docker-compose exec web ash

ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること

今回もテストコードからです。「ポストする」ボタンにはpost_buttonidを付与するとしましょう。

spec/system/07_posts_spec.rb
  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がないので怒られました。ので、ボタンを追加しましょう。

app/views/posts/index.html.erb
  <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的には以下のような感じになっているはず!
image.png

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を更新しましょう。

app/controllers/posts_controller.rb
  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を設定する方法も考えられますが、フォームの内容はユーザー側で簡単に改変できてしまうので違うユーザーのポストとして登録されてしまう危険性があります。
そのため、今回はサーバー側で処理をするようにしてみました。

config/routes.rb
  Rails.application.routes.draw do
    ...
    get   '/posts', to: 'posts#index',  as: :posts
+   post  '/posts', to: 'posts#create', as: :create_post
  end
app/views/posts/index.html.erb
...
- <%= 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.savecontentが未入力の場合にエラーになるようにPostモデルのcontent属性にpresenceを定義しましょう。

app/models/post.rb
  class Post < ApplicationRecord
    belongs_to :user
+
+   validates :content,
+     presence: true
  end

Postモデルにはpresenceのバリデーションを定義します。

最後にエラーメッセージを表示できるようにViewを更新します。

app/views/posts/index.html.erb
  <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で定義していますので、更新していきます。

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で実行するようにリファクタします。

app/controllers/posts_controller.rb
  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文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること

まずはテストです!

spec/system/07_posts_spec.rb
  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.savetrueだったのでしょう。
バリデーションを追加してみます。

app/models/post.rb
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になりました!

ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること

テストから。

spec/system/07_posts_spec.rb
  ...
  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-itemclassを割り当てて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を初期化できそうです。

app/controllers/posts_controller.rb
  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を作成日降順で取得するようにしましょう。

app/controllers/posts_controller.rb
  class PostsController < ApplicationController
    ...
    def index
      @post = Post.new
+     # 更新日時降順で全てのポストを@postsに代入する
+     @posts = Post.order(created_at: :desc)
    end
    ...
  end

全てのポストを作成日時降順で@postsに入れてます。
続いてはこのインスタンス変数を使ってViewを作っていきます。

app/views/posts/index.html.erb
  <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.namepostの外部キー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環境でポストを未入力で「ポストする」ボタンをクリックしてみましょう。
image.png
ふむふむ。<% @posts.each do |post| %>のところでundefined method 'each'が起きてますね。

この2つのテストはposts#createアクションで@post.savefalseの時にrender :indexでレンダリングさせているケースです。
もう一度コードをよくみると、このレンダリングまでにこのアクションでは@postsというインスタンス変数を定義していません。
今回の例外は@postsというモデルオブジェクトのインスタンス変数がないにもかかわらず@posts.eachを使おうとしていることから起きた例外と推測できます。

posts#createアクションを見直してみましょう。

app/controllers/posts_controller.rb
  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.savefalseの場合、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."のポストが成功しているはずなのにページに表示されていないようです。
image.png
スクリーンショットをみると、確かに一番上のポストの文字列が改行されていないHello. World.という文字列になっていますね。

実はpost.contentのような書き方では文字列を表示することはできるのですが、改行がうまく表現できません。
これを解決するために、今回はsafe_joinメソッドを使って改行を正しく表示できるようにします。(参考: 「simple_format」や「safe_join」を使って、正常に改行表示させる方法(Rails) - りょうたくの技術ブログ

app/views/posts/index.html.erb
- <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

無事テストをパスすることができました!!
このように、テストで用いるデータの種類をいろいろなケース用意することで本当に期待通りの動作をしているかを正しく把握し実装することができるのです。

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

ここからはポストページの表示系ですね。
まずは投稿日時降順で表示ができているかという点です。複数ユーザーが投稿していることにします。

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: 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_listi番目にある要素が新しい方から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をつけることにします。

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: 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-nameclassの要素が見つからないようですね。現在ポストしたユーザーの名前を表示している要素にpost-user-nameclassのリンクを定義しましょう。

app/views/posts/index.html.erb
- <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

本日のソースコード

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