Chapter 22

4-3. 学習ログ詳細機能作成

Masuyama
Masuyama
2022.10.15に更新

学習ログ詳細機能作成

今回は、投稿した学習ログの詳細ページ作成していきましょう。

実装

ルーティング

前回追加した recources :postsshow アクションも追加してあげましょう。

config/routes.rb

Rails.application.routes.draw do
  devise_for :users
  root 'home#top'

  resources :posts, only: [:new, :create, :show] # 追加
end

これで、/posts/:id というパスへアクセスすると学習ログの詳細ページのアクションへ飛ぶようになります。
ページを表示するためには show アクションが必要となるので Controller にアクションと、対応する View を追加していきましょう。

Controller (#show)

PostsController に show アクションを用意してあげます。
外部から呼び出すアクション (メソッド )なので、private の上に書くことに注意してください。

app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :authenticate_user!

  ...

  def show # 追加
    @post = Post.find_by(id: params[:id])
  end

  private

  def post_params
    params.require(:post).permit(:title, :content)
  end
end

/posts/:id:id 部分をパラメータとして受け取り、該当する Post を取得しています。

また、TechLog では投稿の閲覧についてはログインを必要としないため、authenticate_user!から show アクションは対象外とします。

class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:show] # 修正
...

次は View を用意しましょう。

View

show アクションの view ファイルは用意されていないので、ファイルを作成してあげましょう。

$ touch app/views/posts/show.html.erb

中身は次のように編集してください。

app/views/posts/show.html.erb

<div class="space-y-6 w-3/4 max-w-lg">
  <label class="block text-xl font-bold text-gray-700">学習ログ詳細</label>
  <div class="items-center justify-center">
      <div tabindex="0" aria-label="card 1" class="focus:outline-none mb-7 bg-white p-6 shadow rounded">
        <div class="flex items-center border-b border-gray-200 pb-6">
          <div class="flex items-start justify-between w-full">
            <div class="pl-3">
              <p class="focus:outline-none text-lg font-medium leading-5 text-gray-800"><%= link_to @post.title, post_path(@post) %></p>
              <p class="focus:outline-none text-sm leading-normal pt-2 text-gray-500">by <%= @post.user.nickname %></p>
            </div>
          </div>
        </div>
        <div class="px-2">
          <p class="focus:outline-none text-sm leading-5 py-4 text-gray-600"><%= @post.content %></p>
        </div>
      </div>
  </div>
</div>

動作確認

では、実際に画面で確認していきましょう。

ログイン時

開発用サーバを起動し、投稿画面から何らかの投稿をします。
その後、http://localhost:3000/posts/xxへアクセスすると投稿の詳細画面が開きます。
(xxの部分は投稿回数に応じて数字を置き換えてください。わからなければ1を当てはめてください)

詳細ページへアクセスすると、このような見た目になっています。

image

ログアウト時

先ほどの投稿詳細画面の URL を一旦コピーしてからログアウトします。
その後、もう一度同じ URL にアクセスしましょう。
投稿画面とは異なり、ログインページへリダイレクトされずにそのまま詳細画面が表示されます。

image

ここまで画面で確認できれば OK です。

ナビゲーションバー(ログイン時)

ログイン時は「ログ投稿」というリンクが表示されていることを確認します。

image

ナビゲーションバー(ログアウト時)

ログアウト時は「ログ投稿」というリンクが表示されていないことを確認します。

image

テスト

さて、実装は完了したので上記の動作を保証するテストを書いていきましょう。

Request Spec

まずは、新しく作った詳細ページへのアクセスを確認する Request Spec を追加していきます。
今回は事前に Post が一つ以上は存在している必要があるため、Post の FactoryBot を使っていることに注意しましょう。

spec/requests/posts_spec.rb

require 'rails_helper'

RSpec.describe 'Posts', type: :request do
  before do # beforeブロックを修正
    @user = create(:user)
    @post = create(:post) # 追加
  end
...
  describe 'GET /posts/:id' do
    context 'ログインしていない場合' do
      it 'HTTPステータス200を返す' do
        get "/posts/#{@post.id}"
        expect(response).to have_http_status '200'
      end
    end
    context 'ログインしている場合' do
      it 'HTTPステータス200を返す' do
        sign_in @user
        get "/posts/#{@post.id}"
        expect(response).to have_http_status '200'
      end
    end
  end

Request Spec の全文は以下のようになります。

<details>
<summary>spec/requests/posts_spec.rb</summary>

require 'rails_helper'

RSpec.describe 'Posts', type: :request do
  before do
    @user = create(:user) # 各テストで使用できるユーザーを作成
    @post = create(:post) # 閲覧用の Post を作成
  end

  describe 'GET /posts/new' do
    context 'ログインしていない場合' do
      it 'HTTPステータス302を返す' do
        get '/posts/new'
        expect(response).to have_http_status(302)
      end

      it 'ログインページにリダイレクトされる' do
        get '/posts/new'
        expect(response).to redirect_to '/users/sign_in'
      end
    end

    context 'ログインしている場合' do
      before { sign_in @user }
      it 'HTTPステータス200を返す' do
        get '/posts/new'
        expect(response).to have_http_status(200)
      end

      it 'ログインページにリダイレクトされない' do
        get '/posts/new'
        expect(response).not_to redirect_to '/users/sign_in'
      end
    end
  end

  describe 'GET /posts/:id' do
    context 'ログインしていない場合' do
      it 'HTTPステータス200を返す' do
        get "/posts/#{@post.id}"
        expect(response).to have_http_status '200'
      end
    end
    context 'ログインしている場合' do
      it 'HTTPステータス200を返す' do
        sign_in @user
        get "/posts/#{@post.id}"
        expect(response).to have_http_status '200'
      end
    end
  end
end

</details>

ログイン状態によらずリダイレクトされずアクセスできることを含め、アクセス性を確認するテストを作成しました。
テストを実行してみてください。
すべて成功すれば OK です。

$ bin/rspec spec/requests/posts_spec.rb
DEBUGGER: Attaching after process 8205 fork to child process 10953
Running via Spring preloader in process 10953

Posts
  GET /posts/new
    ログインしていない場合
      HTTPステータス302を返す
      ログインページにリダイレクトされる
    ログインしている場合
      HTTPステータス200を返す
      ログインページにリダイレクトされない
  GET /posts/:id
    ログインしていない場合
      HTTPステータス200を返す
    ログインしている場合
      HTTPステータス200を返す

Finished in 0.51404 seconds (files took 0.70362 seconds to load)
6 examples, 0 failures

これで Request Spec は完了です。
次は System Spec を作成してあげましょう。

System Spec

こちらも Factory Bot を用いて事前に Post を1つ作成してから、詳細ページの表示内容を確認するテストにします。
System Spec に以下のテストを追記します。

spec/system/posts_spec.rb

require 'rails_helper'

describe 'Post', type: :system do
  before do
    driven_by :selenium_chrome_headless # ヘッドレスモードで実行
    @user = create(:user) # ログイン用ユーザー作成
    @post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id) # 追加
  end
  ...

  ####### ここから追加 #######
  describe 'ログ詳細機能の検証' do
    before { visit "/posts/#{@post.id}" }

    it 'Postの詳細が表示される' do
      expect(page).to have_content('RSpec学習完了')
      expect(page).to have_content('System Specを作成した')
      expect(page).to have_content(@user.nickname)
    end
  end
end

この時点での System Spec の全文は以下の通りです。

<details>
<summary>spec/system/posts_spec.rb</summary>

require 'rails_helper'

describe 'Post', type: :system do
  before do
    driven_by :selenium_chrome_headless # ヘッドレスモードで実行
    @user = create(:user) # ログイン用ユーザー作成
    @post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id)
  end

  # 投稿フォーム
  let(:title) { 'テストタイトル' }
  let(:content) { 'テスト本文' }

  describe 'ログ投稿機能の検証' do
    # ログ投稿を行う一連の操作を subject にまとめる
    subject do
      fill_in 'post_title', with: title
      fill_in 'post_content', with: content
      click_button 'ログを記録'
    end

    context 'ログインしていない場合' do
      before { visit '/posts/new' }
      it 'ログインページへリダイレクトする' do
        expect(current_path).to eq('/users/sign_in')
        expect(page).to have_content('ログインしてください。')
      end
    end

    context 'ログインしている場合' do
      before do
        sign_in @user
        visit '/posts/new'
      end
      it 'ログインページへリダイレクトしない' do
        expect(current_path).not_to eq('/users/sign_in')
      end

      context 'パラメータが正常な場合' do
        it 'Postを作成できる' do
          expect { subject }.to change(Post, :count).by(1)
          expect(current_path).to eq('/')
          expect(page).to have_content('投稿しました')
        end
      end

      context 'パラメータが異常な場合' do
        let(:title) { nil }
        it 'Postを作成できない' do
          expect { subject }.not_to change(Post, :count)
          expect(page).to have_content('投稿に失敗しました')
        end
        it '入力していた内容は維持される' do
          subject
          expect(page).to have_field('post_content', with: content)
        end
      end
    end
  end

  describe 'ログ詳細機能の検証' do
    before { visit "/posts/#{@post.id}" }

    it 'Postの詳細が表示される' do
      expect(page).to have_content('RSpec学習完了')
      expect(page).to have_content('System Specを作成した')
      expect(page).to have_content(@user.nickname)
    end
  end
end

</details>

テスト実行

最後に、すべてのテストに通ることを確認しておきます。

$ bin/rspec
...
Finished in 12 seconds (files took 0.69987 seconds to load)
51 examples, 0 failures

変更をコミット

ここまでの変更をコミットしておきます。

$ git add .
$ git commit -m "学習ログ詳細機能を作成" 
$ git push