LoginSignup
6
7

More than 5 years have passed since last update.

factory_girlでcustom strategyを作るTips

Posted at

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
6
7
0

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
  3. You can use dark theme
What you can do with signing up
6
7