FactoryGirlのcreate strategyを追ってみる

FactoryGirlで、associationメソッドのaliasでuserみたいにassociationのある関連データを生成できるようになった。

動的メソッド感がプンプンするので、知りたいと思ったのでこれを追ってみることにする。

まずはrunnerのrunメソッドから。ここから全てが始まる...(と予想した)

# factory_girl/lib/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

runnerというものが渡されてrunメソッドが走る。runメソッドってなんや。

答えはlib/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

どうやらFactoryRunnerクラスのインスタンスメソッドのようだ。

FactoryRunnerインスタンスが生成された時に、どうやら@traitsも初期化されるようになってるみたいだ。

ふむふむ。

それってrunnerのことだよね。じゃあこのインスタンスメソッドを読んだrunnerってどこで作られてんだ?

ふーむ。わからん。ということで、Railsでの実際の使い方に立ち戻ってみよう。例えば、こんな感じ。

FactoryGirl.define do
  factory :owner do
    user trait: :with_info
    # association :user, factory: :user, trait: :with_info
    workable_date "毎週水曜日10:00~17:00と毎週木曜日の12:00~15:00"
    status "active"

    trait :inactive do
      status "inactive"
    end
  end
end

まずはfactoryメソッドを調べてみることにした。

# lib/factory_girl/syntax/default.rb
module FactoryGirl
  module Syntax
    module Default
      include Methods

      def define(&block)
        DSL.run(block)
      end

      def modify(&block)
        ModifyDSL.run(block)
      end

      class DSL
        def factory(name, options = {}, &block)
          factory = Factory.new(name, options)
          proxy = FactoryGirl::DefinitionProxy.new(factory.definition)
          proxy.instance_eval(&block) if block_given?

          FactoryGirl.register_factory(factory)

          proxy.child_factories.each do |(child_name, child_options, child_block)|
            parent_factory = child_options.delete(:parent) || name
            factory(child_name, child_options.merge(parent: parent_factory), &child_block)
          end
        end

        def sequence(name, *args, &block)
          FactoryGirl.register_sequence(Sequence.new(name, *args, &block))
        end

        def trait(name, &block)
          FactoryGirl.register_trait(Trait.new(name, &block))
        end
# ...

わお!DSLクラスなんてものがある。しかもこれはDefaultモジュールの中で定義されてるぞ〜〜〜。

そのDSLクラスの中のfactoryメソッドがポイントみたいだ。こいつは、factoryするモデルの名前とオプション、あと肝心な中身を受け取って、Factoryクラスのインスタンスを生成している。

ここのfactoryメソッドproxy.instance_eval(&block)だ〜〜〜〜。ここ!

FactoryGirl::DefinitionProxyをみてみると、sequenceとか、よくみるDSLメソッドが定義してある。ファイルは、lib/factory_girl/definition_proxy.rbに。

その中の注目はもちろんmethod_missing!!!

# ...
    def method_missing(name, *args, &block)
      if args.empty? && block.nil?
        @definition.declare_attribute(Declaration::Implicit.new(name, @definition, @ignore))
      elsif args.first.respond_to?(:has_key?) && args.first.has_key?(:factory)
        association(name, *args)
      else
        add_attribute(name, *args, &block)
      end
    end
# ...    

引数に&blockもある。block形式での定義もできる工夫だなー。

argsには実際に生成されるデータの中身がくる。*argsとなっているのでこれらを配列で受け取ってるね。①argsとblockどちらも渡してないとき、②(ここの条件がよくわからない)、③それ以外、で処理を振り分けてますね。

多分②の条件は、ちゃんとそのフィールドがあるかどうか確認してるのだと思う。

associationメソッドが気になってるので、見てみることにする。

# lib/factory_girl/definition_proxy.rb
    def association(name, *options)
      @definition.declare_attribute(Declaration::Association.new(name, *options))
    end

Declaration::Association.newすると、引数はインスタンス変数@optionsに入れられる。

declare_attributeメソッドでは、@declarations配列に引数が入れられる。この引数は、Declaration::Associationインスタンス

うん?これでおわり?もしかしてfactoryメソッドの重要な役割は、@declaration配列にfactoryの宣言情報をボンボン入れていくことなのか...。

そうか、実際に作られるのは、FactoryGirlのクラスメソッドであるcreateメソッドとか、buildメソッドが呼ばれたときかな。。と予測してそちらを見に行く。

これかなーーー。

# lib/factory_girl/evaluation.rb
require 'observer'

module FactoryGirl
  class Evaluation
    include Observable

    def initialize(attribute_assigner, to_create)
      @attribute_assigner = attribute_assigner
      @to_create = to_create
    end

    delegate :object, :hash, to: :@attribute_assigner

    def create(result_instance)
      @to_create[result_instance]
    end
    
    # ...
  end
end

うーむ、行き詰まってしまった。。。

ということで続きはまた明日。