FactoryGirl 源码浅析

FactoryGirl 是我个人十分喜欢的 Gem,它能很方便地模拟测试数据。使用技巧可见 FactoryGirl 技巧。出于兴趣我研究了下它的源代码,说实话比想象得要复杂。由于 FactoryGirl 配置的灵活性以及 Ruby 本身的语言特点,使得它代码整体上比较飘逸。
这里我会对它最基础的方法进行分析,版本为 4.8.0。

FactoryGirl.define

先从定义 Factory 开始。下面的代码定义了名为 user 的 Factory,它有一个名为 name 的属性,值为 Kay。

1
2
3
4
5
FactoryGirl.define do
factory :user do
name 'Kay'
end
end

我们先找到 FactoryGirl.define 的入口,在 syntax/default.rb 里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# syntax/default.rb
module FactoryGirl
module Syntax
module Default
include Methods

def define(&block)
DSL.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 self.run(block)
new.instance_eval(&block)
end
end
end
end
extend Syntax::Default
end

从上面的代码分析可得,define 会调用 DSL.run,之后调用了 DSL#factory 方法,传入的参数 name 为 :user,block 为 { name 'Kay' }。我们重点看 factory 方法其中 4 行:

1
2
3
4
5
6
# DSL#factory
factory = Factory.new(name, options)
proxy = FactoryGirl::DefinitionProxy.new(factory.definition)
proxy.instance_eval(&block) if block_given?

FactoryGirl.register_factory(factory)

首先,会创建一个新的 Factory 对象,然后通过代理类 FactoryGirl::DefinitionProxy 对其进行封装,并执行其中的 block,最后注册这个 factory。其中 proxy.instance_eval(&block) if block_given? 是属性赋值的关键。

{ name 'Kay' } 会调用 FactoryGirl::DefinitionProxy 的 name 方法,但由于代理类没有该方法,最终会执行 method_missing,而该方法实现了赋值的逻辑。

1
2
3
4
5
6
7
8
9
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 或者 association,这里对几种情况分别进行了处理。

FactoryGirl.define 的分析差不多结束了,method_missing 这一元编程的魔法在这里又发挥了巨大的作用。

FactoryGirl.create

讲完了 Factory 是如何定义的,我们来研究下如何创建一个 Factory。

1
user = FactoryGirl.create(:user, name: 'Link')

我们首先得找到 create 的入口。当搜寻了一遍之后会发现,并没有显式定义 create 的地方。看来该方法是动态定义的了。从官方文档上来看,create 定义在 FactoryGirl::Syntax::Methods。搜索之后发现 StrategySyntaxMethodRegistrar 里出现了给它动态添加方法的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# strategy_syntax_method_registrar.rb
module FactoryGirl
private
def define_singular_strategy_method
strategy_name = @strategy_name

define_syntax_method(strategy_name) do |name, *traits_and_overrides, &block|
FactoryRunner.new(name, strategy_name, traits_and_overrides).run(&block)
end
end

def define_syntax_method(name, &block)
FactoryGirl::Syntax::Methods.module_exec do
if method_defined?(name) || private_method_defined?(name)
undef_method(name)
end

define_method(name, &block)
end
end
end
end

用 create 替换 strategy_name,FactoryGirl.create 实质调用了 FactoryRunner.new(name, :create, : traits_and_overrides).run(&block)。而 FactoryRunner#run 在进行了一些准备后,最终调用了 factory.run(runner_strategy, @overrides, &block) 来创建对象。

1
2
3
4
5
6
7
8
9
10
11
# Factory#run
def run(build_strategy, overrides, &block)
block ||= ->(result) { result }
compile
strategy = StrategyCalculator.new(build_strategy).strategy.new
evaluator = evaluator_class.new(strategy, overrides.symbolize_keys)
attribute_assigner = AttributeAssigner.new(evaluator, build_class, &compiled_constructor)
evaluation = Evaluation.new(attribute_assigner, compiled_to_create)
evaluation.add_observer(CallbacksObserver.new(callbacks, evaluator))
strategy.result(evaluation).tap(&block)
end

最后一行 strategy.result(evaluation).tap(&block) 最终返回的是 evaluation.object,而 evaluation 把 object 委托给 attribute_assigner。

1
2
3
4
5
6
7
8
9
10
# AttributeAssigner#object
def object
@evaluator.instance = build_class_instance
build_class_instance.tap do |instance|
attributes_to_set_on_instance.each do |attribute|
instance.public_send("#{attribute}=", get(attribute))
@attribute_names_assigned << attribute
end
end
end

build_class_instance 实质上调用了 User.new 方法,创建了 User 对象。instance.public_send 对该对象的属性进行赋值。

至此最关键的创建步骤说完了,我们简单地说下其他几行的作用。
compile 主要处理 Factory 之间的继承关系。
StrategyCalculator.new(build_strategy).strategy.new 为简单工厂,可以根据 Strategy 的名字找到对应的类。
evaluator 保存了属性的赋值。为什么不直接进行赋值,而要使用一个中间类?我觉得原因是,FactoryGirl 创建 user 时不仅可以给 user 的属性赋值,还可以给 evaluator 赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
factory :user do
name "John Doe"
factory :user_with_posts do
transient do
posts_count 5
end
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
end
end
end

# 调用
create(:user_with_posts, posts_count: 15)

这里的 posts_count 不是 user 本身的属性,而属于 evaluator。
AttributeAssigner 创建了 user 对象,并给它赋值。
Evaluation 是对 Strategy 回调方法的一个封装,比如 Strategy::Create,都是直接调用的 evaluation 的方法。

1
2
3
4
5
6
7
8
9
# Strategy::Create#result
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

顺带说下,compiled_constructor、compiled_to_create 在经过千辛万苦的查找后,是两个非常简单的 block。compiled_constructor 为 { new },而 compiled_to_create 为 { |instance| instance.save! },来自 configuration.rb 19、20 行。

总结

由于水平精力有限,只简单地分析了最基础的 define 和 create 方法。
define 使用了 method_missing 来实现属性的赋值。由于 FactoryGirl create 对象时能通过很灵活的方式,比如 trait,使得其代码在创建对象时要考虑各方面的配置。这里只抽出了最主要的流程进行说明。