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
end
FactoryBot.define do
factory :teacher do
password {'abcdefg'}
password_confirmation {'abcdefg'}
end
end
TeacherFactoryHash = {
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
長くなったので今日はここまで