ログインできるようになったので,モデルを順次追加していく. その前に,モデルのテストを簡単にするために,FactoryBot を利用する. トラブルシューティングを含め,細かく書いてあるので, Rails + RSpec で FactoryBot(旧 FactoryGirl)を使う にしたがって作業する.
FactoryBot のインストール
-
Gemfile に factory_bot を追加する.
group :development, :test do gem 'factory_bot_rails', '~> 4.0'
-
bundle する(2行目以降は結果: 追加分のみ)
$ 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
-
.rspec の require が spec_helper になっているので,rails_helper に修正する
--require rails_helper --format documentation
-
spec/rails_helper.rb に factory_bot.rb の require を追加する.また,後で spec/support 以下にファイルを置くので,そこのファイルを自動的に require する設定を追記する(コメントしてあるだけなので,コメントを外せばよい)
# 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 }
-
config/application.rb に fixture_replacement の記述を追加する.以前,generators の設定をしているので,そこに追加する形にすればよい.また,FactoryBot の設定に合わせて,generators の設定も修正しておく.
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 (後略)
key based FactoryBot の設定
FactoryBot 自体の機能で関連などを簡単に作成できる. しかし,自動的に作成される関連では uniqueness などの制約テストがやりにくい. そこで,私の場合は自作の key based な FactoryBot の作り込みでテストを運用している. key based FactoryBot のためには,既存の FactoryBot が存在した時に,それを返す find_or_create に相当するメソッドが必要となる. maunovaha/fg_find_or_create.rb を参考に,少し改変して取り入れる.
-
spec/factories および spec/support/create_factories ディレクトリを作成する.
mkdir -p spec/{factories,support/create_factories}
-
spec/support/fb_find_or_create.rb を作成する.
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
-
spec/factories/teachers.rb を作成する.テストに関係しない属性は FactoryBot.define の方に書いておくと楽になる.
FactoryBot.define do factory :teacher do password {'abcdefg'} password_confirmation {'abcdefg'} end end
-
spec/support/create_factories/teacher_factory.rb を作成する.このメソッドでは,key に対応した FactoryBot が生成済みであれば,既存の FactoryBot を取得する.ない場合には key に対応した属性で新規 FactoryBot を生成する.
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
shared_examples の作成
毎回似たような仕様を書くのは面倒なので,RSpec では共通にできる example を用意することができる. ここでは私が普段よく利用する shared_examples を掲載しておく.
-
spec/support/shared_examples.rb を以下のように用意する.長くなるのでこれの使い方は次回に回す.
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
長くなったので今日はここまで