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
うーむ、行き詰まってしまった。。。
ということで続きはまた明日。