rubyのテスト(rspec)でテストデータを用いるときには、大体factory_girlかfixturesを使うことが多いと思いますが、factory_girlは独自DSLが多く、学習コストがかかりちょっと面倒に思うことが多々ありました。(何度も使うのを辞めようかなと思ったか...)
特にデータ作成時に毎回、FactoryGirl.create
を使ってたところ、テストの総合計時間がえらいほど掛かり、これを何とかしなければと思い、いろいろ検討した結果、create
を使わず、stub
やbuild
等で予めモックデータを作成しておいて、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実行後の結果処理
- after_buildをcallback
after(:build)で定義したブロック - before_createをcallback
before(:create)で定義したブロック - model.createを実行
- 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実行後の結果処理
- 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実行後の結果処理
- stub_database_interaction_on_resultメソッドをcall
activerecordのスタブメソッドを実装、id,create_atを擬似生成している - clear_changed_attributes_on_resultメソッドをcall
- 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
主にやっていることは
- 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