TL;DR
FactoryGirl 试图通过做一个非常大的假设来提供帮助
创建它的“存根”对象。即:
you have an id, which means you are not a new record, and thus are already persisted!
不幸的是,ActiveRecord 使用它来决定是否应该
keep persistence up to date。
所以存根模型会尝试将记录持久化到数据库中。
请不要尝试将 RSpec 存根/模拟填充到 FactoryGirl 工厂中。
这样做会在同一个对象上混合两种不同的存根哲学。挑选
一个或另一个。
RSpec 模拟只应该在规范的某些部分使用
生命周期。将它们搬入工厂建立了一个环境,该环境将
隐藏对设计的违反。由此导致的错误将是
令人困惑且难以追踪。
如果您查看包含 RSpec 的文档,请说
test/unit,
您可以看到它提供了确保模拟正确的方法
在测试之间设置和拆除。将模型放入工厂
不保证会发生这种情况。
这里有几个选项:
-
不要使用 FactoryGirl 创建存根;使用存根库
(rspec-mocks、minitest/mocks、mocha、flexmock、rr 等)
如果您想将模型属性逻辑保留在 FactoryGirl 中,那很好。
为此目的使用它并在其他地方创建存根:
stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)
是的,您必须手动创建关联。这不是一件坏事,
进一步讨论见下文。
-
清除id 字段
after(:stub) do |order, evaluator|
order.id = nil
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
-
创建您自己的new_record?定义
factory :order do
ignore do
line_items_count 1
new_record true
end
after(:stub) do |order, evaluator|
order.define_singleton_method(:new_record?) do
evaluator.new_record
end
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
end
这里发生了什么?
IMO,尝试创建“存根”has_many 通常不是一个好主意
与FactoryGirl 关联。这往往会导致更紧密耦合的代码
并且可能会不必要地创建许多嵌套对象。
要了解这个位置,以及 FactoryGirl 的情况,我们需要
看几件事:
- 数据库持久层/gem(即
ActiveRecord,Mongoid,
DataMapper、ROM 等)
- 任何存根/模拟库(mintest/mocks、rspec、mocha 等)
- 模拟/存根服务的目的
数据库持久层
每个数据库持久层的行为都不同。事实上,许多人的行为
主要版本之间有所不同。 FactoryGirl 尝试不做假设
关于该层是如何设置的。这给了他们最大的灵活性
长途。
假设:我猜你在剩下的时间里使用ActiveRecord
这个讨论。
在我撰写本文时,ActiveRecord 的当前 GA 版本是 4.1.0。什么时候
你在上面设置了一个has_many 关联,
there's
a
lot
that
goes
on.
这在旧版 AR 中也略有不同。里面很不一样
Mongoid 等。期望 FactoryGirl 理解
所有这些宝石的错综复杂,版本之间也没有差异。就是这样
碰巧has_many association's writer
尝试keep persistence up to date。
你可能会想:“但我可以用存根设置逆”
FactoryGirl.define do
factory :line_item do
association :order, factory: :order, strategy: :stub
end
end
li = build_stubbed(:line_item)
是的,这是真的。虽然只是因为 AR 决定了not to
persist。
事实证明,这种行为是一件好事。不然会很
很难在不频繁访问数据库的情况下设置临时对象。
此外,它允许将多个对象保存在一个
事务,如果出现问题,则回滚整个事务。
现在,您可能在想:“我完全可以将对象添加到 has_many,而无需
打数据库”
order = Order.new
li = order.line_items.build(name: 'test')
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 1
li = LineItem.new(name: 'bar')
order.line_items << li
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 2
li = LineItem.new(name: 'foo')
order.line_items.concat(li)
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 3
order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 5
是的,但这里 order.line_items 真的是
ActiveRecord::Associations::CollectionProxy。
它定义了它自己的build,
#<<,
和#concat
方法。当然,这些真的都是委托给协会定义的,
has_many 是等效的方法:
ActiveRecord::Associations::CollectionAssocation#build
和ActiveRecord::Associations::CollectionAssocation#concat。
这些按顺序考虑了基本模型实例的当前状态
决定是现在还是以后坚持。
FactoryGirl 在这里真正能做的就是让底层类的行为
定义应该发生的事情。实际上,这可以让您使用 FactoryGirl 来
generate any class,不是
只是数据库模型。
FactoryGirl 确实尝试在保存对象方面提供一些帮助。这主要是
在工厂的create 一侧。根据他们的维基页面
interaction with ActiveRecord:
...[a factory] 首先保存关联,以便外键正确
设置依赖模型。要创建一个实例,它调用 new 没有任何
参数,分配每个属性(包括关联),然后调用
节省!。 factory_girl 没有做任何特别的事情来创建 ActiveRecord
实例。它不与数据库交互或扩展 ActiveRecord 或
你的模型以任何方式。
等等!您可能已经注意到,在上面的示例中,我漏掉了以下内容:
order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 5
是的,没错。我们可以将order.line_items= 设置为一个数组,但它不是
坚持!那么是什么给了?
存根/模拟库
有许多不同的类型,FactoryGirl 都适用于它们。为什么?
因为 FactoryGirl 对它们中的任何一个都不做任何事情。完全是
不知道你有哪个库。
请记住,您将 FactoryGirl 语法添加到您的 test library of choice。
您不会将您的库添加到 FactoryGirl。
如果 FactoryGirl 没有使用您喜欢的库,它在做什么?
模拟/存根服务的目的
在了解底层细节之前,我们需要定义what
a
"stub"
is
及其intended purpose:
存根为测试期间拨打的电话提供预设答案,通常不
对任何超出测试程序的内容做出响应。
存根还可以记录有关呼叫的信息,例如电子邮件网关存根
记住它“发送”的消息,或者可能只记住它的多少条消息
'发送'。
这与“模拟”略有不同:
模拟...:预先编程的对象形成一个期望
他们预计会收到的电话的规范。
存根是一种通过预设响应设置协作者的方法。坚持
只有您为特定测试触摸的协作者公共 API 才会保留
存根轻巧小巧。
无需任何“存根”库,您就可以轻松创建自己的存根:
stubbed_object = Object.new
stubbed_object.define_singleton_method(:name) { 'Stubbly' }
stubbed_object.define_singleton_method(:quantity) { 123 }
stubbed_object.name # => 'Stubbly'
stubbed_object.quantity # => 123
因为 FactoryGirl 在涉及到他们的
“存根”,这是the approach they take。
查看 FactoryGirl v.4.4.0 实现,我们可以看到
当你build_stubbed时,以下方法都被存根:
persisted?
new_record?
save
destroy
connection
reload
update_attribute
update_column
created_at
这些都是非常ActiveRecord-y。但是,正如您在has_many 中看到的那样,
这是一个相当有漏洞的抽象。 ActiveRecord 公共 API 表面积为
很大。期望一个库完全覆盖它是不完全合理的。
为什么 has_many 关联不适用于 FactoryGirl 存根?
如上所述,ActiveRecord 检查它的状态以决定它是否应该
keep persistence up to date。
由于stubbed definition of new_record?
设置任何has_many 都会触发数据库操作。
def new_record?
id.nil?
end
在我抛出一些修复之前,我想回到 stub 的定义:
存根为测试期间拨打的电话提供预设答案,通常不
完全不响应任何超出测试程序的内容。
存根还可以记录有关呼叫的信息,例如电子邮件网关存根
记住它“发送”的消息,或者可能只记住它的多少条消息
'发送'。
存根的 FactoryGirl 实现违反了这一原则。由于它没有
知道你将在你的测试/规范中做什么,它只是试图
阻止数据库访问。
修复 #1:不要使用 FactoryGirl 创建存根
如果您希望创建/使用存根,请使用专用于该任务的库。自从
看来您已经在使用 RSpec,使用它的 double 功能(以及新的验证
instance_double,
class_double,
还有object_double
在 RSpec 3) 中。或者
使用 Mocha、Flexmock、RR 或其他任何东西。
您甚至可以创建自己的超级简单存根工厂(是的,存在一些问题
这只是用罐头制作对象的简单方法的示例
回复):
require 'ostruct'
def create_stub(stubbed_attributes)
OpenStruct.new(stubbed_attributes)
end
FactoryGirl 让您可以非常轻松地创建 100 个模型对象
需要 1. 当然,这是一个负责任的使用问题;一如既往的强大
创造责任。很容易忽视深层嵌套
关联,它们并不真正属于存根。
此外,正如您所注意到的,FactoryGirl 的“存根”抽象有点
泄漏迫使您了解其实现和您的数据库
持久层的内部结构。使用存根库应该完全解放你
没有这种依赖。
如果您想将模型属性逻辑保留在 FactoryGirl 中,那很好。
为此目的使用它并在其他地方创建存根:
stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)
是的,您必须手动设置关联。虽然你只设置
测试/规范所需的那些关联。你没有得到其他5个
那些你不需要的。
拥有一个真正的存根库有助于明确说明这一点。
这是您的测试/规格,可为您提供有关设计选择的反馈。带一个
像这样的设置,规范的读者可以提出以下问题:“为什么我们需要 5
订单项?” 如果它对规范很重要,那么它就在前面
并且很明显。否则,它不应该存在。
对于那些称为单个对象的长链方法也是如此,
或后续对象的一系列方法,可能是时候停止了。这
law of demeter 有帮助吗
你,不妨碍你。
修复 #2:清除 id 字段
这更像是一种黑客行为。我们知道默认存根设置了id。因此,我们
只需将其删除即可。
after(:stub) do |order, evaluator|
order.id = nil
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
我们永远不能有一个返回 id 并设置一个 has_many 的存根
协会。 FactoryGirl完整设置的new_record?的定义
防止这种情况发生。
修复 #3:创建您自己的 new_record? 定义
在这里,我们将id 的概念与存根是
new_record?。我们将其推送到一个模块中,以便我们可以在其他地方重用它。
module SettableNewRecord
def new_record?
@new_record
end
def new_record=(state)
@new_record = !!state
end
end
factory :order do
ignore do
line_items_count 1
new_record true
end
after(:stub) do |order, evaluator|
order.singleton_class.prepend(SettableNewRecord)
order.new_record = evaluator.new_record
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
end
我们仍然需要为每个模型手动添加它。