LoginSignup
7

More than 5 years have passed since last update.

posted at

factory_girlでcustom strategyを作るTips

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

主にやっていることは
- 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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7