给定两个对象之间的标准has_many关系。举个简单的例子,我们来看看:
class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
end
我想要做的是生成带有存根订单项列表的存根订单。
FactoryGirl.define do
factory :line_item do
name 'An Item'
quantity 1
end
end
FactoryGirl.define do
factory :order do
ignore do
line_items_count 1
end
after(:stub) do |order, evaluator|
order.line_items = build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)
end
end
end
上面的代码不起作用,因为Rails想要在分配line_items时调用order并且FactoryGirl引发异常:
RuntimeError: stubbed models are not allowed to access the database
那么你怎么(或者有可能)生成一个存根对象,其中has_may集合也是存根的?
答案 0 :(得分:111)
FactoryGirl尝试通过做出非常大的假设来提供帮助
创造它&#34; stub&#34;对象。即:那:
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,尝试创建一个&#34; stubbed&#34;一般不是一个好主意。 has_many
与FactoryGirl
的关联。这往往会导致更紧密耦合的代码
并且可能会不必要地创建许多嵌套对象。
要理解这个位置,以及FactoryGirl的情况,我们需要 看看几件事:
ActiveRecord
,Mongoid
,
DataMapper
,ROM
等)每个数据库持久层的行为都不同。事实上,许多人表现得很好 主要版本之间不同。 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。
你可能会想:&#34;但我可以用存根&#34;
设置反转FactoryGirl.define do
factory :line_item do
association :order, factory: :order, strategy: :stub
end
end
li = build_stubbed(:line_item)
是的,这是真的。虽然这只是因为AR决定not to
persist。
事实证明这种行为是一件好事。否则,它会非常
很难设置临时对象而不经常访问数据库。
此外,它允许将多个对象保存在一个对象中
事务,如果出现问题,则回滚整个事务。
现在,您可能会想:&#34;我完全可以在没有has_many
的情况下添加对象
点击数据库&#34;
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:
... [工厂]首先保存关联,以便正确使用外键 在依赖模型上设置。要创建实例,它将调用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:
存根为测试期间拨打的电话提供固定答案,通常不会 完全响应任何超出测试编程范围的任何东西。 存根还可以记录有关呼叫的信息,例如电子邮件网关存根 它会记住它发送的消息,或者可能只消息它有多少消息 &#39;发送&#39;
这与&#34; mock&#34;:
略有不同模拟 ...:预先编程的对象形成了一个期望 他们期望收到的电话的规范。
Stubs可以作为一种使用预设回复设置协作者的方法。坚持 只有您为特定测试触摸的协作者公共API保留 存根轻巧小巧。
没有任何&#34;存根&#34;库,您可以轻松创建自己的存根:
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完全与库无关 &#34;存根&#34;,这是the approach they take。
看看FactoryGirl v.4.4.0的实现,我们可以看到了
当build_stubbed
:
persisted?
new_record?
save
destroy
connection
reload
update_attribute
update_column
craeted_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
的定义:
Stubs为测试期间拨打的电话提供固定答案,通常不是 完全响应任何超出测试编程范围的任何东西。 存根还可以记录有关呼叫的信息,例如电子邮件网关存根 它会记住它发送的消息,或者可能只消息它有多少消息 &#39;发送&#39;
存根的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&#34; stub&#34;抽象有点 泄漏迫使您了解其实现和数据库 持久层的内部。使用存根lib应该完全解放你 从而具有这种依赖性。
如果你想让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个 那些你不需要的。
有一个真正的存根lib有助于明确说明。 这是您的测试/规格,为您提供有关您的设计选择的反馈。有了 像这样的设置,规范的读者可以提出这样的问题:&#34;为什么我们需要5 订单项?&#34; 如果它对规范很重要,那么它就在前面很棒 而且很明显。否则,它不应该在那里。
同样的事情适用于那些称为单个对象的长链方法, 或后续对象的一系列方法,可能是时候停止了。该 law of demeter可以提供帮助 你,不要阻碍你。
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?
的定义
防止这种情况。
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
我们仍然需要为每个模型手动添加它。
答案 1 :(得分:11)
我看到这个答案浮出水面,但遇到了同样的问题: FactoryGirl: Populate a has many relation preserving build strategy
我发现最干净的方法是明确地删除关联调用。
require 'rspec/mocks/standalone'
FactoryGirl.define do
factory :order do
ignore do
line_items_count 1
end
after(:stub) do |order, evaluator|
order.stub(line_items).and_return(FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order))
end
end
end
希望有所帮助!
答案 2 :(得分:1)
我发现Bryce的解决方案是最优雅的,但它会产生关于新allow()
语法的弃用警告。
为了使用新的(更干净的)语法,我做了这个:
更新06/05/2014:我的第一个提议是使用私有api方法,感谢Aaraon K提供更好的解决方案,请阅读评论以供进一步讨论
#spec/support/initializers/factory_girl.rb
...
#this line enables us to use allow() method in factories
FactoryGirl::SyntaxRunner.include(RSpec::Mocks::ExampleMethods)
...
#spec/factories/order_factory.rb
...
FactoryGirl.define do
factory :order do
ignore do
line_items_count 1
end
after(:stub) do |order, evaluator|
items = FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)
allow(order).to receive(:line_items).and_return(items)
end
end
end
...