Ruby
RSpec
FactoryGril

factory_girlでcustom strategyを作るTips

More than 1 year has passed since last update.

rubyのテスト(rspec)でテストデータを用いるときには、大体factory_girlかfixturesを使うことが多いと思いますが、factory_girlは独自DSLが多く、学習コストがかかりちょっと面倒に思うことが多々ありました。(何度も使うのを辞めようかなと思ったか...)

特にデータ作成時に毎回、FactoryGirl.createを使ってたところ、テストの総合計時間がえらいほど掛かり、これを何とかしなければと思い、いろいろ検討した結果、createを使わず、stubbuild等で予めモックデータを作成しておいて、activerecord-importを用いてbulk insertしちゃえば、2倍以上insertコストが減らすことが出来ました。


各strategyが何をやってるのか


  • 最新version(4.7)より抜粋


create


factory_girl/strategy/create.rb

module FactoryGirl

module Strategy
class Create
def association(runner)
runner.run
end

def result(evaluation)
evaluation.object.tap do |instance|
evaluation.notify(:after_build, instance)
evaluation.notify(:before_create, instance)
evaluation.create(instance)
evaluation.notify(:after_create, instance)
end
end
end
end
end



strategy実行後の結果処理


  1. after_buildをcallback

    after(:build)で定義したブロック

  2. before_createをcallback

    before(:create)で定義したブロック

  3. model.createを実行

  4. after(:create)をcallback

    after(:create)で定義したブロック


build


factory_girl/strategy/build.rb

module FactoryGirl

module Strategy
class Build
def association(runner)
runner.run
end

def result(evaluation)
evaluation.object.tap do |instance|
evaluation.notify(:after_build, instance)
end
end
end
end
end



strategy実行後の結果処理


  1. after_buldをcallback

    after(:build)で定義したブロック


build_stub


factory_girl/strategy/stub.rb

module FactoryGirl

module Strategy
class Stub
@@next_id = 1000

def association(runner)
runner.run(:build_stubbed)
end

def result(evaluation)
evaluation.object.tap do |instance|
stub_database_interaction_on_result(instance)
clear_changed_attributes_on_result(instance)
evaluation.notify(:after_stub, instance)
end
end

private
... # 省略
end



strategy実行後の結果処理


  1. stub_database_interaction_on_resultメソッドをcall

    activerecordのスタブメソッドを実装、id,create_atを擬似生成している

  2. clear_changed_attributes_on_resultメソッドをcall


  3. after_stubをcallback

    after(:stub)で定義したブロック


runnerがやってる処理


factory_girl/factory_runner.rb

module FactoryGirl

class FactoryRunner
def initialize(name, strategy, traits_and_overrides)
@name = name
@strategy = strategy

@overrides = traits_and_overrides.extract_options!
@traits = traits_and_overrides
end

def run(runner_strategy = @strategy, &block)
factory = FactoryGirl.factory_by_name(@name)

factory.compile

if @traits.any?
factory = factory.with_traits(@traits)
end

instrumentation_payload = {
name: @name,
strategy: runner_strategy,
traits: @traits,
overrides: @overrides,
factory: factory
}

ActiveSupport::Notifications.instrument('factory_girl.run_factory', instrumentation_payload) do
factory.run(runner_strategy, @overrides, &block)
end
end
end
end



  • 定義されてる名前のFactoryをfind

  • Factoryをcompile

  • traitがあれば追記

  • Factoryを実行(ActiveSupport::Notificationsでイベント計測)


独自のStrategyを定義する


Strategyの作成例


custom_strategy.rb

class CustomStub < FactoryGirl::Strategy::Stub

def association(runner)
runner.run(:build_stubbed)
end

def result(evaluation)
evaluation.object.tap do |instance|
evaluation.notify(:before_stub_custom, instance)
stub_database_interaction_on_result(instance)
evaluation.notify(:after_stub_custom, instance)
end
end

end

# ここで実際に作成したクラスを登録する
FactoryGirl.register_strategy(:stub_custom, CustomStub)
FactoryGirl.register_callback(:before_stub_custom)
FactoryGirl.register_callback(:after_stub_custom)



  • associationメソッドでStrategyを決めて実行(既に登録済みのもの)

  • notifyでcallbackを行う名前と引数を渡す

  • register_strategyでStrategyの登録

  • register_callbackでcallbackの登録


自動挿入フィールドを設定するCustom Strategy

https://gist.github.com/kitaro-tn/980bba8f979b595e904871617ebf8319

主にやっていることは

- idのauto_increment

- created_at, updated_atの挿入

- delete_flagのデフォルト挿入


使用例


サンプルModel Userフィールド


  • name

  • age

  • height

  • weight

  • bmi

  • delete_flag

  • updated_at

  • created_at


factory/sample_user.rb

FactoryGirl.define do

factory :user do
name 'Bob'
age 31
height 172
weight 64

before(:stub_custom) do |user, evaluation|
user.bmi = (user.weight / ((user.height / 100.0) ** 2)).round(1)

end

before(:build_custom) do |user, evaluation|
user.bmi = (user.weight / ((user.height / 100.0) ** 2)).round(1)

end

end
end

# 作成したStrategyを使用することにより、id, delete_flag, created_at, updated_atを自動挿入したインスタンスを返す
user = FactoryGirl.stub_custom(:user)

# 各フィールドに抜けのないインスタンスが10生成される
users = FactoryGirl.stub_custom_list(:user, 10)



データを一括登録

上記のようにCustom Strategyを使用すれば、not nullフィールドに値を設定できるようになるので、後は利用する数だけインスタンスを生成して、その後にactiverecord-importを利用してbulk insertをすれば、登録するテーブル数にはよりますが、FactoryGirlでいちいちcreateするより、かなり高速にデータが登録できるはずです


sample_import.rb

require 'activerecord-import'

users = FactoryGirl.stub_custom_list(:user, 10)

User.import users