factory_girlでcustom strategyを作るTips


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

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

## 各strategyが何をやってるのか

* 最新version(4.7)より抜粋


### create

```ruby: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

```ruby: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

```ruby: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がやってる処理

```ruby: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の作成例

```ruby: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


```ruby: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するより、かなり高速にデータが登録できるはずです

```ruby:sample_import.rb
require 'activerecord-import'

users = FactoryGirl.stub_custom_list(:user, 10)

User.import users
```