FactoryBot とRSpec によるテスト(2) - 不定期刊 Rails App を作る(13)

RailsApp Rails FactoryBot RSpec 2018年 11月 21日

teacher_spec の記述

昨日の shared_example を使った Teacher モデルのテストを作成する.

  1. spec/models がなければ作る
  2. mkdir -p spec/models
  3. spec/models/teacher_spec.rb を以下のように記載する. 実際には行わないテストも記載してあるが,既存のテンプレートから手動でコピーしたからである. 後日,このテンプレートの設定方法については説明する. 元々のソースにはコメントは書いてないが,説明のためにソースファイル内に解説をコメントとして掲示しておく.
  4. require 'rails_helper'
    
    # Teacher model の仕様を開始
    RSpec.describe Teacher, type: :model do
    
      # validation の共通テスト (使わないものはコメントしてある)
      context 'common validation check' do
        # テスト対象を指定する.今回は :hkob というキーで指定された FactoryBot である
        subject { teacher_factory :hkob }
    
        # ここから下に出てくる it_behaves_like は shared_examples.rb に記載した共有テストである.
        # 最初の引数が共有テストの名前,その後ろがそのテストに渡す引数である.
    
        # name と email 属性が nil の場合に validate に失敗することを確認(presence validation)
        # FactoryBot の作成確認のため,全ての属性が正しい場合に有効であることも確認している
        it_behaves_like :presence_validates, %i[name email]
        # email が重複している場合に validate に失敗することを確認 (unique validation)
        it_behaves_like :unique_validates, %i[email], -> { teacher_factory :taro }
        #it_behaves_like :plural_unique_validates, %i[], -> { teacher_factory :other }
        # モデルオブジェクトが削除できるかを確認 (destroy validation)
        it_behaves_like :destroy_validates
        #it_behaves_like :reject_destroy_validates
        #it_behaves_like :belongs_to, :teacher, has_many: %i[], has_one: %i[], children: :optional, child: :optional
        #it_behaves_like :dependent_destroy, :teacher, %i[has_many has_one]
        #it_behaves_like :reject_destroy_for_relations, :teacher, %i[has_many has_one]
        #it_behaves_like :destroy_nullify_for_relations, :teacher, %i[has_many has_one]
      end
    
      # 複数の FactoryBot を生成した後に実行できるメソッドの仕様
      context 'after some teachers are registrered' do
        # 準備する FactoryBot の key を配列で指定しておく
        model_keys = %i[hkob taro]
        # 上記で作成した key 配列に対応した FactoryBot を targets として事前準備する.
        # let! なので,it の前に必ず準備された状態になる(遅延実行ではない).
        let!(:targets) { teacher_factories model_keys }
    
        # クラスメソッドに対する仕様 (今回はなし)
        describe 'Teacher class' do
          # テスト対象を Teacher クラスとする.
          subject { Teacher }
    
          # 今回はテストはない
          #it_behaves_like :mst_block, -> t do
          #  {
          #    method1: [v1, a1, v2, a2, ...],
          #    method2: [v1, a1, v2, a2, ...],
          #end
          #
          #it_behaves_like :msta_block, -> t do
          #  {
          #    method1: [v1, a1, v2, a2, ...],
          #    method2: [v1, a1, v2, a2, ...],
          #  }
          #end
          #
          #it_behaves_like :mst, :METHOD1, -> { [v1, teacher_factories(model_keys.values_at()), v2, teacher_factories(model_keys.values_at())] }
          #it_behaves_like :msta, :METHOD1, -> { [v1, teacher_factories(model_keys.values_at()), v2, teacher_factories(model_keys.values_at())] }
        end
    
        # クラスインスタンスに対する仕様
        context 'Teacher instances' do
          # 上記で作成した複数の FactoryBot をテスト対象とする.
          subject { targets }
    
          # amst は array method send test の略で配列の個々の要素に method を呼び出します.
          # ハッシュの中身は,キーが method 名, 値の配列が引数とその結果を並べたものです.
          # 引数がない場合には,引数の部分は nil とします.
          # 最初のテストでは targets のそれぞれに対して,name を渡した時に,その属性値である name の値が返ってくることを期待します.
          # ただし,この example 内で it は内部に一つだけしか存在しないため,block の最初だけしか targets が作られない.
          # インスタンスの値を変えるような副作用があるメソッドを読んだ場合には,後続のメソッドは副作用の影響を受けることに注意が必要である.
          # 逆に各メソッドのテストの度にインスタンスが作られないので,ベタに it を並べるよりもテストは高速化される.
          it_behaves_like :amst_block, -> t do
            {
              name: [nil, %w[小林弘幸 高専太郎]],
              email: [nil, %w[hkob@example.com taro@example.com]],
            }
          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
  5. これを保存すると裏で動作していた guard による自動テストが駆動する.devise の設定でほとんどのテストは通過するが,name は後から追加した属性であるためテストに失敗している.
  6. 16:00:22 - INFO - Running: spec/models/teacher_spec.rb
    Running via Spring preloader in process 50685
    
    Teacher
      common validation check
        behaves like presence_validates
          should be valid
          when name is nil
            should not be valid (FAILED - 1)
          when email is nil
            should not be valid
        behaves like unique_validates
          when another object has same email
            should not be valid
        behaves like destroy_validates
          should destroy
            should change `Teacher.count` by -1
      after some teachers are registrered
        Teacher instances
          behaves like amst_block
    amst: name
    amst: email
            should receive above methods(amst)
    
    Failures:
    
      1) Teacher common validation check behaves like presence_validates when name is nil should not be valid
         Failure/Error: is_expected.to_not be_valid
           expected #<Teacher id: 114, name: nil, email: "hkob@example.com", created_at: "2018-11-14 07:00:24", updated_at: "2018-11-14 07:00:24"> not to be valid
         Shared Example Group: :presence_validates called from ./spec/models/teacher_spec.rb:8
         # ./spec/support/shared_examples.rb:10:in `block (4 levels) in <main>'
         # -e:1:in `<main>'
    
    Finished in 0.26226 seconds (files took 0.8522 seconds to load)
    6 examples, 1 failure
    
    Failed examples:
  7. name が nil の場合に validation が失敗する必要がある.app/models/teacher.rb に validates を追加する.
  8. class Teacher < ApplicationRecord
      # Include default devise modules. Others available are:
      # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
      devise :database_authenticatable, :registerable,
             :recoverable, :rememberable, :validatable
    
      validates :name, presence: {message: 'は空にはできません.'}
    end
  9. 保存をすると,また guard が起動し,全てのテストが通過する.

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