RailsApp Rails FactoryBot RSpec 2018年 11月 20日
ログインできるようになったので,モデルを順次追加していく. その前に,モデルのテストを簡単にするために,FactoryBot を利用する. トラブルシューティングを含め,細かく書いてあるので, Rails + RSpec で FactoryBot(旧 FactoryGirl)を使う にしたがって作業する.
group :development, :test do
  gem 'factory_bot_rails', '~> 4.0'$ bundle
Fetching factory_bot 4.11.1
Installing factory_bot 4.11.1
Fetching factory_bot_rails 4.11.1
Installing factory_bot_rails 4.11.1--require rails_helper
--format documentation# Add additional requires below this line. Rails is not loaded until this point!
require 'factory_bot'
(中略)
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }Rails.application.config.generators do |g|
  g.fixture_replacement :factory_bot, dir: 'spec/factories'
  g.factory_girl    true
  g.test_framework :rspec,
    fixtures: true,
    view_specs: false,
    routing_specs: false,
    controller_specs: false,
    request_specs: true
  (後略)FactoryBot 自体の機能で関連などを簡単に作成できる. しかし,自動的に作成される関連では uniqueness などの制約テストがやりにくい. そこで,私の場合は自作の key based な FactoryBot の作り込みでテストを運用している. key based FactoryBot のためには,既存の FactoryBot が存在した時に,それを返す find_or_create に相当するメソッドが必要となる. maunovaha/fg_find_or_create.rb を参考に,少し改変して取り入れる.
mkdir -p spec/{factories,support/create_factories}module FactoryBot::Syntax::Methods
  def find_or_create(name, *attributes, &block)
    attributes = FactoryBot.attributes_for(name, *attributes)
    klass = FactoryBot.factory_by_name(name).build_class
    enums = klass.defined_enums
    find_attributes = attributes.clone
    find_attributes.delete(:password)
    find_attributes.delete(:password_confirmation)
    find_attributes.keys.each do |key|
      find_attributes[key] = enums[key.to_s][find_attributes[key]] if enums.has_key?(key.to_s)
    end
    result = klass.find_by(find_attributes, &block)
    result || FactoryBot.create(name, attributes, &block)
  end
endFactoryBot.define do
  factory :teacher do
    password {'abcdefg'}
    password_confirmation {'abcdefg'}
  end
endTeacherFactoryHash = {
  hkob: %w[小林弘幸 hkob@example.com],
  tmcit: %w[高専太郎 taro@example.com],
}
# @param [Symbol] key オブジェクトを一意に決定するキー
# @return [User] Teacher FactoryBot オブジェクト
def teacher_factory(key)
  n, e = TeacherFactoryHash[key.to_sym]
  FactoryBot.find_or_create(
    :user,
    name: n,
    email: e,
  ) if n
end
# @param [Array<Symbol, String>] keys オブジェクトを一意に決定するキーの配列
# @return [Array<Teacher>] Teacher FactoryBot オブジェクトの配列
def teacher_factories(keys)
  keys.map { |k| teacher_factory(k) }
end
# @return [Array<Teacher>] Teacher FactoryBot オブジェクトの配列
def teacher_all_factories
  teacher_factories TeacherFactoryHash.keys
end毎回似たような仕様を書くのは面倒なので,RSpec では共通にできる example を用意することができる. ここでは私が普段よく利用する shared_examples を掲載しておく.
shared_examples_for :presence_validates do |keys|
  # keys を指定しない場合にも,valid だけは確認
  it { is_expected.to be_valid }
  # keys が指定された時には keys を一つずつ外しながら invalid を確認
  if keys
    keys.each do |key|
      context "when #{key} is nil" do
        it do
          subject[key] = nil
          is_expected.to_not be_valid
        end
      end
    end
  end
end
shared_examples_for :unique_validates do |keys, block|
  keys.each do |key|
    context "when another object has same #{key}" do
      it do
        another_object = block.call
        subject[key] = another_object[key]
        is_expected.to_not be_valid
      end
    end
  end
end
shared_examples_for :plural_unique_validates do |keys, block|
  context "when another object has same #{keys.join ', '}" do
    it do
      another_object = block.call
      keys.each do |key|
        subject[key] = another_object[key]
      end
      is_expected.to_not be_valid
    end
  end
  context "when another object has at least one different keys(#{keys.join ', '})" do
    it do
      keys.each do |except_key|
        another_object = block.call
        compare = true
        keys.each do |key|
          if key == except_key
            compare = false if subject[key] == another_object[key]
          else
            another_object[key] = subject[key]
          end
        end
        expect(another_object).to be_valid if compare
        another_object.destroy
      end
    end
  end
end
shared_examples_for :destroy_validates do
  context 'should destroy' do
    it do
      klass = subject.class
      expect { subject.destroy }.to change(klass, :count).by -1
    end
  end
end
shared_examples_for :reject_destroy_validates do
  context 'should not destroy' do
    it do
      klass = subject.class
      expect { subject.destroy }.not_to change(klass, :count)
    end
  end
end
shared_examples_for :belongs_to do |model, hash|
  has_many_relations = hash[:has_many]
  has_one_relations = hash[:has_one]
  children = hash[:children] || model.to_s.pluralize
  child = hash[:child] || model
  context "when destroying #{model}" do
    if has_many_relations
      has_many_relations.each do |relation|
        it "#{relation}.#{children}.count should decrease" do
          parent = subject.send(relation)
          expect { subject.destroy }.to change(parent.send(children), :count).by(-1)
        end
      end
    end
    if has_one_relations
      has_one_relations.each do |relation|
        it "#{relation}.#{child} should be nil" do
          parent = subject.send(relation)
          subject.destroy
          expect(parent.send(child, :true)).to be_nil
        end
      end
    end
  end
end
shared_examples_for :dependent_destroy do |model, relations|
  relations.each do |relation|
    context "when destroying #{relation}" do
      it 'should be destroyed by dependency' do
        parent = subject.send(relation)
        expect { parent.destroy }.to change(model.to_s.pluralize.classify.constantize, :count).by(-1)
      end
    end
  end
end
shared_examples_for :destroy_nullify_for_relations do |model, relations|
  relations.each do |relation|
    context "when destroying #{model}.#{relation}" do
      it "#{model} should set null to #{relation}" do
        parent = subject.send(relation)
        parent.destroy
        expect(subject.reload.send(relation)).to be_nil
      end
    end
  end
end
shared_examples_for :reject_destroy_for_relations do |model, relations|
  relations.each do |relation|
    context "when destroying #{model}.#{relation}" do
      it "#{model} should reject destroying #{relation}" do
        parent = subject.send(relation)
        parent.destroy
        expect(parent.errors[:base].size).to eq(1)
      end
    end
  end
end
shared_examples_for :mst_block do |block|
  it "should receive above methods(mst)" do
    block.call(targets).each do |method, array|
      print "mst: #{method}\n"
      array.each_slice(2) do |(v, a)|
        expect(subject.send(method, *v)).to eq a
      end
    end
  end
end
shared_examples_for :msta_block do |block|
  it "should receive above methods(msta)" do
    block.call(targets).each do |method, array|
      print "msta: #{method}\n"
      array.each_slice(2) do |(v, a)|
        expect(subject.send(method, *v)).to match_array a
      end
    end
  end
end
shared_examples_for :amst_block do |block|
  it "should receive above methods(amst)" do
    block.call(targets).each do |method, array|
      print "amst: #{method}\n"
      array.each_slice(2) do |(v, a)|
        expect(subject.map { |o| o.send(method, *v) }).to eq a
      end
    end
  end
end
shared_examples_for :amsta_block do |block|
  it "should receive above methods(amsta)" do
    block.call(targets).each do |method, array|
      print "amsta: #{method}\n"
      array.each_slice(2) do |(v, a)|
        answers = subject.map { |o| o.send(method, *v) }
        answers.zip(a).each do |ans, a_a|
          expect(ans).to match_array a_a
        end
      end
    end
  end
end
shared_examples_for :mst do |method, block|
  it "should receive #{method}" do
    array = block.call
    array.each_slice(2) do |(v, a)|
      expect(subject.send(method, *v)).to eq a
    end
  end
end
shared_examples_for :msta do |method, block|
  it "should receive #{method}" do
    array = block.call
    array.each_slice(2) do |(v, a)|
      expect(subject.send(method, *v)).to match_array a
    end
  end
end
shared_examples_for :amst do |method, block|
  it "should receive #{method}" do
    array = block.call
    array.each_slice(2) do |(v, a)|
      expect(subject.map { |o| o.send(method, *v) }).to eq a
    end
  end
end
shared_examples_for :amsta do |method, block|
  it "should receive #{method}" do
    array = block.call
    array.each_slice(2) do |(v, a)|
      answers = subject.map { |o| o.send(method, *v) }
      answers.zip(a).each do |ans, a_a|
        expect(ans).to match_array a_a
      end
    end
  end
end
shared_context :prepare_factories do |symbols, block|
  symbols.each do |symbol|
    let!(symbol) { block.call(symbol) }
  end
end
shared_examples_for :gp do |klass|
  subject { klass } if klass
  it 'should receive gp' do
    expect(subject.gp).to eq [subject, "#{ subject.name.underscore }_id".to_sym]
    expect(subject.gp(false)).to eq [subject, "#{ subject.name.underscore }_id".to_sym]
    expect(subject.gp(true)).to eq [subject, :id]
  end
end
shared_examples_for :response_status_check do |value|
  it "response should be #{value}" do
    subject.call; expect(response.status).to eq value
  end
end
shared_examples_for :response_body_includes do |strs|
  it "response body includes #{strs}" do
    subject.call
    Array(strs).each { |str| expect(response.body).to include str }
  end
end
shared_examples_for :response_body_not_includes do |strs|
  it "response body does not include #{strs}" do
    subject.call
    Array(strs).each { |str| expect(response.body).not_to include str }
  end
end
shared_examples_for :redirect_to do
  it { subject.call; expect(response).to redirect_to(return_path) }
end
shared_examples_for :increment_object_by_create do |klass|
  it { expect { subject.call }.to change(klass, :count).by 1 }
end
shared_examples_for :not_increment_object_by_create do |klass|
  it { expect { subject.call }.not_to change(klass, :count) }
end
shared_examples_for :decrement_object_by_destroy do |klass|
  it { expect { subject.call }.to change(klass, :count).by -1 }
end
shared_examples_for :change_object_count_by_create_or_destroy do |klass, n|
  it { expect { subject.call }.to change(klass, :count).by n }
end
shared_examples_for :change_object_value_by_update do |klass, key, value|
  it do
    pre_value = object.send(key)
    expect { subject.call }.to change { klass.find(object.id).send(key) }.from(pre_value).to(value)
  end
end
shared_examples_for :not_change_object_value_by_update do |klass, key|
  it { expect { subject.call }.not_to change { klass.find(object.id).send(key) } }
end
shared_examples_for :flash_notice_message do |str|
  it { subject.call; expect(flash.now[:notice]).to eq str }
end
shared_examples_for :flash_alert_message do |str|
  it { subject.call; expect(flash.now[:alert]).to eq str }
end長くなったので今日はここまで