Kurasu モデルのテストと実装 - 不定期刊 Rails App を作る(15)

RailsApp Rails FactoryBot RSpec 2018年 11月 23日

kurasu_spec.rb の修正

昨日生成した kurasu_spec をクラスに合わせて設定する.

  1. spec/factories/kurasu.rb に書かれている属性の初期値を全て消す.
  2. FactoryBot.define do
      factory :kurasu do
      end
    end
  3. spec/models/kurasu_spec.rb を編集する.最初に以下の文よりも上の部分を spec/support/kurasu_factory.rb に保存する.
  4. ##### ↑ write to spec/support/create_factories/kurasu_factory.rb
  5. テストに耐えうるデータを spec/support/create_factories/kursu_factory.rb に作成する.
  6. KurasuFactoryHash = {
      hkob2300: %w[hkob 2300 f],
      hkob5300o: %w[hkob 5300 t],
      taro3300: %w[taro 3300 f]
    }
    
    # @param [Symbol, String] key オブジェクトを一意に決定するキー
    # @return [Kurasu] Kurasu FactoryGirl オブジェクト
    def kurasu_factory(key)
      t, n, o = KurasuFactoryHash[key.to_sym]
      FactoryBot.find_or_create(
        :kurasu,
        teacher_id: teacher_factory(t.to_sym).id,
        name: n,
        obsolete: o == 't',
      ) if t
    end
    
    # @param [Array<Symbol, String>] keys オブジェクトを一意に決定するキーの配列
    # @return [Array<Kurasu>] Kurasu FactoryGirl オブジェクトの配列
    def kurasu_factories(keys)
      keys.map { |k| kurasu_factory(k) }
    end
  7. spec/models/kurasu_spec.rb を記述する.前回と同様ソースにはコメントを書いていないが,説明のためにコメントを追加した.また,わかりきっている amst_block の name のようなテストは普段はやらないが,今回は説明のために記述している.個別の詳細はソース内のコメントを参照のこと.
  8. require 'rails_helper'
    
    RSpec.describe Kurasu, type: :model do
      context 'common validation check' do
        subject { kurasu_factory :hkob2300 }
    
        # name, obsolete, teacher_id 属性が nil の場合に validate に失敗することを確認(presence validation)
        it_behaves_like :presence_validates, %i[name obsolete teacher_id]
        #it_behaves_like :unique_validates, %i[], -> { kurasu_factory :other }
        # name, teacher_id の組み合わせたが重複している場合に validate に失敗することを確認 (plural unique validation)
        it_behaves_like :plural_unique_validates, %i[name teacher_id], -> { kurasu_factory :taro3300 }
        # モデルオブジェクトが削除できるかを確認 (destroy validation)
        it_behaves_like :destroy_validates
        #it_behaves_like :reject_destroy_validates
        # teacher への belong_to が正しく動作するか,また削除した時に teacher.kurasus が一つ減るかを確認
        it_behaves_like :belongs_to, :kurasu, has_many: %i[teacher]
        # teacher を削除した時に,連動して kurasu が削除されるかを確認
        it_behaves_like :dependent_destroy, :kurasu, %i[teacher]
        #it_behaves_like :reject_destroy_for_relations, :kurasu, %i[has_many has_one]
        #it_behaves_like :destroy_nullify_for_relations, :kurasu, %i[has_many has_one]
      end
    
      context 'after some kurasus are registrered' do
        model_keys = %i[hkob2300 hkob5300o taro3300]
        let!(:targets) { kurasu_factories model_keys }
    
        describe 'Kurasu class' do
          subject { Kurasu }
    
          # order_name の scope の確認
          it_behaves_like :mst_block, -> t do
            {
              # 名前の順でソートできるか
              order_name: [nil, t.values_at(0, 2, 1)],
            }
          end
    
          # not_obsolete, teacher_is の scope の確認
          it_behaves_like :msta_block, -> t do
            {
              # 有効なものだけ取得できるか
              not_obsolete: [nil, t.values_at(0, 2)],
              # teacher が hkob のものだけ取得できるか
              teacher_is: [t.first.teacher, t.values_at(0, 1)],
            }
          end
          #it_behaves_like :mst, :METHOD1, -> { [v1, kurasu_factories(model_keys.values_at()), v2, kurasu_factories(model_keys.values_at())] }
          #it_behaves_like :msta, :METHOD1, -> { [v1, kurasu_factories(model_keys.values_at()), v2, kurasu_factories(model_keys.values_at())] }
        end
    
        context 'Kurasu instances' do
          subject { targets }
    
          # name の確認
          it_behaves_like :amst_block, -> t do
            {
              name: [nil, %w[2300 5300 3300]],
            }
          end
    
          #it_behaves_like :amsta_block, -> t do
          #  {
          #    method1: [v1, a1, v2, a2, ...],
          #    method2: [v1, a1, v2, a2, ...],
          #  }
          #end
          #it_behaves_like :amst, :METHOD1, -> { [v1, a1, v2, a2] }
          #it_behaves_like :amsta, :METHOD1, -> { [v1, a1, v2, a2] }
        end
      end
    end
  9. この状況での guard の出力(仕様)はこんな感じになる.かなりの部分がエラーになっていることが確認できる(計8箇所).
  10. Kurasu
      common validation check
        behaves like presence_validates
          should be valid
          when name is nil
            should not be valid (FAILED - 1)
          when obsolete is nil
            should not be valid (FAILED - 2)
          when teacher_id is nil
            should not be valid (FAILED - 3)
        behaves like plural_unique_validates
          when another object has same name, teacher_id
            should not be valid (FAILED - 4)
          when another object has at least one different keys(name, teacher_id)
            should be valid
        behaves like destroy_validates
          should destroy
            should change `Kurasu.count` by -1
        behaves like belongs_to
          when destroying kurasu
            teacher.kurasus.count should decrease (FAILED - 5)
        behaves like dependent_destroy
          when destroying teacher
            should be destroyed by dependency (FAILED - 6)
      after some kurasus are registrered
        Kurasu class
          behaves like mst_block
    mst: order_name
            should receive above methods(mst) (FAILED - 7)
          behaves like msta_block
            should receive above methods(msta) (FAILED - 8)
        Kurasu instances
          behaves like amst_block
    amst: name
            should receive above methods(amst)

kurasu.rb の修正

kurasu_spec のテストが通過するように app/models/kurasu.rb に実装を記述する.

  1. 最初の 3 つのエラーは behaves like presence_validates のものである. name と teacher_id については,前回の teacher と同様に presence validation を追加するだけでよい.
  2. validates :name, :teacher_id, presence: {message: 'は空にはできません'}
  3. obsolete は boolean なので,presence validation では確認できない(false は presence で判定できないため). そのため,inclusion validation を使う.
  4. validates :obsolete, inclusion: {in: [true, false], message: 'は空にはできません'}
  5. plural unique validation については,uniqueness に scope をつけることで実現する.
  6. validates :name, uniqueness: {scope: :teacher_id, message: '教員とクラスの組はユニークである必要があります.'}
  7. belongs_to と dependent_desctroy の validation はそれぞれ関連を設定すれば満足する.まず,app/models/kurasu.rb の方に belongs_to を追加する.
  8.   # @return [Teacher] 関連する教員
      belongs_to :teacher
  9. app/models/teacher.rb の方には反対側の has_many を追加する.
  10.   # @return [Array<Kurasu>] 関連するクラス一覧
      has_many :kurasus, dependent: :destroy
  11. 残るは三つの scope の設定である.app/models/kurasu.rb に scope を二つ登録する. いつもは scope に関して arel_table を使いまくっているが,今回は初心者用に通常の Rails の書き方で記述している. そもそも arel_table は Rails のプライベートメソッドなので,積極的に使うものではない.
  12.   scope :order_name, -> { order :name }
      scope :not_obsolete, -> { where obsolete: false }
      scope :teacher_is, -> t { where teacher_id: t.id }
  13. これらを修正すると,テストは全て成功する.
  14. Kurasu
      common validation check
        behaves like presence_validates
          should be valid
          when name is nil
            should not be valid
          when obsolete is nil
            should not be valid
          when teacher_id is nil
            should not be valid
        behaves like plural_unique_validates
          when another object has same name, teacher_id
            should not be valid
          when another object has at least one different keys(name, teacher_id)
            should be valid
        behaves like destroy_validates
          should destroy
            should change `Kurasu.count` by -1
        behaves like belongs_to
          when destroying kurasu
            teacher.kurasus.count should decrease
        behaves like dependent_destroy
          when destroying teacher
            should be destroyed by dependency
      after some kurasus are registrered
        Kurasu class
          behaves like mst_block
    mst: order_name
            should receive above methods(mst)
          behaves like msta_block
    msta: not_obsolete
    msta: teacher_is
            should receive above methods(msta)
        Kurasu instances
          behaves like amst_block
    amst: name
            should receive above methods(amst)
    
    Finished in 0.31014 seconds (files took 0.39904 seconds to load)
    12 examples, 0 failures

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