📘 hkob-astro-notion-blog

これまではてなブログにて情報発信をしていましたが、令和5年3月22日より、こちらでの情報発信を始めました。2019年以前の古い記事は過去の Middleman 時代のものなので、情報が古いです。記録のためだけに残しています。

kurasus_controller の request spec(1) - 不定期刊 Rails App を作る(16)

💡
この記事は Middleman 時代に書いた古いものです。記録のため、astro-notion-blog に移行していますが、あまり参考にしないでください。
request spec の設置

Rails 5 からは controller_spec を描くことは非推奨となり,request_spec で記述することが推奨されている(参照: Rails5でコントローラのテストをController specからRequest specに移行する ). request_spec では,controller_spec が行なっていたようなコントローラの内部実装に関わる点はテストから除外し,リクエスト/レスポンスにのみ関心を持つブラックボックステストを行う.

kurasus_controller と integration_test の作成
  1. 最初に kurasus_controller を作成する(2行目以降は結果).
    $ bin/rails g controller kurasus
    Running via Spring preloader in process 63130
          create  app/controllers/kurasus_controller.rb
          invoke  haml
          create    app/views/kurasus
          invoke  rspec
          invoke  assets
          invoke    js
          invoke    css
  2. routes は作ってくれないので,config/routes.rb に resources を追加する.
    # config/routes.rb
    Rails.application.routes.draw do
      devise_for :teachers
      get 'pages/home'
      root to: "pages#home"
      resources :kurasus
    end
  3. integration_test は同時に作成してくれないので,別途作成する(2行目以降は結果).
    $ bin/rails g integration_test kurasus
    Running via Spring preloader in process 70314
          invoke  rspec
          create    spec/requests/kurasus_spec.rb
  4. app/controllers/kurasus_controller.rb を保存した時に,spec/requests/kurasus_spec.rb をテストして欲しいので,Guardfile に設定を追加する.
    watch(rails.controllers) do |m|
      [
        rspec.spec.call("routing/#{m[1]}_routing"),
        rspec.spec.call("controllers/#{m[1]}_controller"),
        rspec.spec.call("acceptance/#{m[1]}"),
        rspec.spec.call("requests/#{m[1]}") # これを追加
      ]
    end
テスト環境の準備
  1. integration_test では spec/requests/kurasus_spec.rb が作成された. 現在はデフォルトのひな形で作成されているが,これから作成する kurasus_spec.rb をベースに後でひな形を新規に作成することにする. このひな形では,index でレスポンス 200 が返ってくることを期待している.
    require 'rails_helper'
    
    RSpec.describe "Kurasus", type: :request do
      describe 'GET #index' do
        it "works! (now write some real specs)" do
          get kurasus_path
          expect(response).to have_http_status(200)
        end
      end
    end
  2. 今回の実装では,ログインしていない場合にクラス一覧は表示できない. このため,ログインしている場合としていない場合で対応が異なる必要があり,それもテストする必要がある. integration_test でログインを実現するために,teacher 権限でログイン/ログアウトをするためのヘルパメソッドを作成しておく. teacher_login 自体は内部で RSpec の before と after を設置するだけである. before では login_teacher_as,after では sign_out_one を呼び出している. 特に sign_out_one の呼び出しは忘れがちなので,ヘルパで一括設定するようにしてしまっている.
    # @param [Symbol] fg_key user の FactoryBot のキー
    # @return [User] ログインしたユーザ
    def login_teacher_as(fg_key)
      @one = user_factory(fg_key)
      sign_in @one
      allow(controller).to receive(:current_teacher).and_return(@one)
      @one
    end
    
    # @note サインアウト
    def sign_out_one
      sign_out @one
    end
    
    # @param [Symbol] fg_key user の FactoryBot のキー
    # @note before と after を自動設置する.
    def teacher_login(fg_key)
      before { login_teacher_as fg_key }
      after { sign_out_one }
    end
/kurasus のテスト

ひとまず /kurasus のテストを書いてみる

  1. spec/requests/kurasus_spec.rb を以下のように変更する.いつものようにソースには書いていないが,説明のためコメントを追加している.
    require 'rails_helper'
    
    RSpec.describe :Kurasus, type: :request do
      # devise の integration test のためのヘルパを追加
      include Devise::Test::IntegrationHelpers
    
      # create, update, destroy 後にリダイレクトするパスを設定しておく(遅延実行なので必要となった時だけ作成される).
      let(:return_path) { kurasus_path(edit_mode: true) }
      # hkob でログインしている時の context
      context 'login by hkob' do
        # 事前に 2300 クラスを作っておく
        let!(:object) { kurasu_factory :hkob2300 }
        # 事前に 5300(obsolete) クラスを作っておく
        let!(:others) { kurasu_factory :hkob5300o }
        # テスト開始時に hkob でログイン,終了後ログアウトを設定
        teacher_login :hkob
    
        describe 'GET #index' do
          # 通常時のコンテキスト (obsolete は表示しない)
          context 'normal mode' do
            # オプションなしで index を描画
            subject { -> { get kurasus_path } }
            # リクエストは正常
            it_behaves_like :response_status_check, 200
            # 2300 クラスは表示
            it_behaves_like :response_body_includes, '2300'
            # 5300 クラスは非表示
            it_behaves_like :response_body_not_includes, '5300'
          end
    
          # obsolete 表示時のコンテキスト (全クラスを表示)
            # obsolete オプションありで index を描画
          context 'edit mode' do
            subject { -> { get kurasus_path(edit_mode: true) } }
            # リクエストは正常
            it_behaves_like :response_status_check, 200
            # 2300, 5300 クラスは共に表示
            it_behaves_like :response_body_includes, %w[2300 5300]
          end
        end
      end
    
      # ログインしていない時の context
      context 'not login' do
        describe 'GET #index' do
          subject { -> { get kurasus_path } }
          # レスポンスはリダイレクト
          it_behaves_like :response_status_check, 302
          # 2300, 5300 クラスは共に非表示
          it_behaves_like :response_body_not_includes, %w[2300 5300]
        end
      end
    end
  2. 保存すると guard の画面に大量にエラーが出力される.エラーの原因は全て同じで以下に示した ActionNotFound である.
    AbstractController::ActionNotFound:
      The action 'index' could not be found for KurasusController
  3. ひとまずテストが動く環境を作ることにする.app/controllers/kurasus_controller.rb に index のメソッドがないので,とりあえず空で作成する
    class KurasusController < ApplicationController
      def index
      end
    end
  4. また,app/views/kurasus/index.html.haml のファイルを空で作成しておく
    touch app/views/kurasus/index.html.haml
  5. この結果,guard の出力は以下のようになった(もし変わっていない場合には,guard のコンソールでリターンキーを一度タイプするとよい).
    Kurasus
      login by hkob
        GET #index
          normal mode
            behaves like response_status_check
              response should be 200
            behaves like response_body_includes
              response body includes ["クラス一覧:", "小林弘幸", "2300"] (FAILED - 1)
            behaves like response_body_not_includes
              response body does not include 5300
          edit mode
            behaves like response_status_check
              response should be 200
            behaves like response_body_includes
              response body includes ["クラス一覧:", "小林弘幸", "2300", "5300"] (FAILED - 2)
      not login
        GET #index
          behaves like response_status_check
            response should be 302 (FAILED - 3)
          behaves like response_body_not_includes
            response body does not include ["クラス一覧:", "小林弘幸", "2300", "5300"]
    (中略)
    Finished in 2.67 seconds (files took 0.21094 seconds to load)
    7 examples, 3 failures
  6. まず一番最後にあるログインしていない時にリダイレクトするテストを通すことにする. これは pages#home と同じで before_action を追加するだけである. この追加によりこのテストは通過した.
    class KurasusController < ApplicationController
      before_action :authenticate_teacher!
  7. 次に edit_mode の時のクラス一覧表示を実装してみる. edit_mode 時には obsolete のクラスも全部表示することとする. まず,app/controllers/kurasus_controller.rb に教員が所有するクラス一覧を取得するコードを追加する.
    def index
      @kurasus = current_teacher.kurasus.order_name
    end
  8. 取得したクラスを表示する view を app/views/kurasus/index.html.haml に追加する. edit_mode の時には編集関係のリンクが発生するようにしている. また,content_for では @title を設定している.これは,app/views/layouts/application.html.haml から呼び出される想定である. t_ars は ActiveRecord の locale を取得するメソッドである.locale から取得することで,属性名のゆらぎを回避できる. さらに,new, show, edit については,各 view のタイトルをそのままリンク名にしている. ただし,destroy と confirm メッセージについては,ページがあるわけではないので,index 内のメッセージを利用する.
    - content_for :title do
      - @title = "#{t '.title'}: #{current_teacher.name}"
    
    %table
      - if @edit_mode
        %caption= link_to t('kurasus.new.title'), new_kurasu_path
      %tr
        - t_ars(Kurasu, %i[name]).each do |w|
          %th= w
        %th= t('control')
      - @kurasus.each do |k|
        %tr
          %td= k.name
          %td
            = link_to t('kurasus.show.title'), kurasu_path(k)
            - if @edit_mode
              = link_to t('kurasus.edit.title'), edit_kurasu_path(k)
              = link_to t('.destroy'), kurasu_path(k), method: :delete, data: {confirm: t('.confirm')}
  9. まず content_for で設定したタイトルをブラウザのタイトルとしたい. そこで,app/views/layouts/application.html.haml の title にて,haml の content_for を呼び出すように変更する.
        %title= content_for?(:title) ? yield(:title) : :Attendance
  10. さらにページタイトルとして yield の前に h1 でタイトルを表示するように変更する.
    %h1= @title
    = yield
  11. jp.kurasus.index.title などの locale を config/locales/views.ja.yml に追加する.
    ja:
      control: 制御
      kurasus:
        index:
          title: クラス一覧
          enter_edit_mode: 編集モードに入る
          destroy: クラス削除
          confirm: クラスを削除してよろしいですか?
        show:
          title: クラス表示
        new:
          title: クラス作成
        edit:
          title: クラス編集
  12. t_ars メソッドは app/helpers/applitaion_helper.rb に追記する.
    # @param [Object] model モデルクラス
    # @param [Array<String>] atr 属性名
    def t_ar(model, atr = nil)
      if atr
        return model.human_attribute_name(atr)
      else
        return model.model_name.human
      end
    end
    
    # @param [Object] model モデルクラス
    # @param [Array<String>] keys 属性名の配列
    # @return [Array<String>] 翻訳後の文字列の配列
    def t_ars(model, keys)
      keys.map { |key| model.human_attribute_name(key) }
    end
  13. jp.activerecord.attributes.kurasu.name の locale を config/locales/models.ja.yml に追加する.
    ja:
      activerecord:
        models:
          kurasu: クラス
        attributes:
          kurasu:
            name: クラス名
            obsolete: 未使用
  14. ここまでの修正の結果,edit_mode における obsolete の絞り込み以外はテスト通過した.
    Kurasus
      login by hkob
        GET #index
          normal mode
            behaves like response_status_check
              response should be 200
            behaves like response_body_includes
              response body includes ["クラス一覧:", "小林弘幸", "2300"]
            behaves like response_body_not_includes
              response body does not include 5300 (FAILED - 1)
          obsolete view mode
            behaves like response_status_check
              response should be 200
            behaves like response_body_includes
              response body includes ["クラス一覧:", "小林弘幸", "2300", "5300"]
      not login
        GET #index
          behaves like response_status_check
            response should be 302
          behaves like response_body_not_includes
            response body does not include ["クラス一覧:", "小林弘幸", "2300", "5300"]
    (中略)
    Finished in 0.63085 seconds (files took 0.79338 seconds to load)
    7 examples, 1 failure
  15. このテストを通過するように app/controllers/kurasus_controller.rb において, edit_mode 時のみに not_obsolete を追加する.
    class KurasusController < ApplicationController
      before_action :authenticate_teacher!
      def index
        @edit_mode = true if params[:edit_mode]
        @kurasus = current_teacher.kurasus.order_name
        @kurasus = @kurasus.not_obsolete unless @edit_mode
      end
    end
  16. これによって全てのテストは通過した.
    Kurasus
      login by hkob
        GET #index
          normal mode
            behaves like response_status_check
              response should be 200
            behaves like response_body_includes
              response body includes ["クラス一覧:", "小林弘幸", "2300"]
            behaves like response_body_not_includes
              response body does not include 5300
          obsolete view mode
            behaves like response_status_check
              response should be 200
            behaves like response_body_includes
              response body includes ["クラス一覧:", "小林弘幸", "2300", "5300"]
      not login
        GET #index
          behaves like response_status_check
            response should be 302
          behaves like response_body_not_includes
            response body does not include ["クラス一覧:", "小林弘幸", "2300", "5300"]
    
    Finished in 0.19479 seconds (files took 0.21901 seconds to load)
    7 examples, 0 failures

長くなったので今日はここまで