Jar*_*red 36 ruby-on-rails factory-bot
给定两个对象之间的标准has_many关系.举个简单的例子,我们来看看:
class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
end
Run Code Online (Sandbox Code Playgroud)
我想要做的是生成带有存根行项目列表的存根订单.
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
Run Code Online (Sandbox Code Playgroud)
上面的代码不起作用,因为Rails想要在分配line_items时调用order并且FactoryGirl引发异常:
RuntimeError: stubbed models are not allowed to access the database
那么你如何(或者可能)生成一个存根对象,其中has_may集合也是存根的?
Aar*_*n K 116
FactoryGirl在创建它的"存根"对象时,通过做出一个非常大的假设来尝试提供帮助.也就是说:
你有一个id
,这意味着你不是一个新的记录,因此已经存在!
不幸的是,ActiveRecord使用它来决定它是否应该 保持最新的持久性.因此,存根模型会尝试将记录持久保存到数据库中.
请不要不尝试垫片RSpec的存根/嘲笑成FactoryGirl工厂.这样做会在同一个对象上混合两种不同的存根理念.选择一个或另一个.
RSpec模拟仅应在规范生命周期的某些部分使用.将它们移到工厂会设置一个隐藏设计违规的环境.由此产生的错误将令人困惑并且难以追踪.
如果你看一下将RSpec包含在说测试/单元中的文档 ,你可以看到它提供了确保模拟在测试之间正确设置和拆除的方法.将模拟物放入工厂并不能保证这种情况发生.
这里有几个选项:
不要使用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)
Run Code Online (Sandbox Code Playgroud)
是的,您必须手动创建关联.这不是一件坏事,请参阅下面的进一步讨论.
清除id
现场
after(:stub) do |order, evaluator|
order.id = nil
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
Run Code Online (Sandbox Code Playgroud)创建自己的定义 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
Run Code Online (Sandbox Code Playgroud)IMO,尝试创建"存根" has_many
关联通常不是一个好主意FactoryGirl
.这往往会导致更紧密耦合的代码,并且可能会不必要地创建许多嵌套对象.
要了解这个位置,以及FactoryGirl的情况,我们需要看一些事情:
ActiveRecord
,Mongoid
,
DataMapper
,ROM
,等等)每个数据库持久层的行为都不同.事实上,许多主要版本之间的行为有所不同.FactoryGirl 试图不对该层的设置方式做出假设.这使他们在长期内具有最大的灵活性.
假设:我猜你正在使用ActiveRecord
这个讨论的其余部分.
在我写这篇文章时,目前的GA版本ActiveRecord
是4.1.0.当你设置一个has_many
它的关联,
有
一个
很多
是
去
上.
在较旧的AR版本中,这也略有不同.它在Mongoid等方面有很大的不同.期望FactoryGirl理解所有这些宝石的复杂性,以及版本之间的差异是不合理的.事实上,该has_many
协会的作者
试图保持最新的持久性.
你可能会想:"但我可以用存根设置反转"
FactoryGirl.define do
factory :line_item do
association :order, factory: :order, strategy: :stub
end
end
li = build_stubbed(:line_item)
Run Code Online (Sandbox Code Playgroud)
是的,这是真的.虽然这只是因为AR决定不 坚持.事实证明这种行为是一件好事.否则,在不经常访问数据库的情况下设置临时对象将非常困难.此外,它允许在单个事务中保存多个对象,如果出现问题则回滚整个事务.
现在,你可能会想:"我完全可以在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
Run Code Online (Sandbox Code Playgroud)
order.line_items
是的,但这里真的是一个
ActiveRecord::Associations::CollectionProxy
.它定义了它自己的build
,
#<<
和#concat
方法.当然,这些真的都委托回定义的关联,这has_many
是等效的方法:
ActiveRecord::Associations::CollectionAssocation#build
和ActiveRecord::Associations::CollectionAssocation#concat
.这些考虑了基本模型实例的当前状态,以便决定是现在还是以后保持.
所有FactoryGirl在这里真的可以让底层类的行为定义应该发生什么.实际上,这允许您使用FactoryGirl 生成任何类,而不仅仅是数据库模型.
FactoryGirl确实试图帮助保存对象.这主要是在create
工厂方面.根据他们与ActiveRecord交互的维基页面
:
... [工厂]首先保存关联,以便在依赖模型上正确设置外键.要创建一个实例,它调用new而不带任何参数,分配每个属性(包括关联),然后调用save!.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
Run Code Online (Sandbox Code Playgroud)
是的,没错.我们可以设置order.line_items=
为一个数组,它不会持久存在!什么给出了什么?
有许多不同的类型,FactoryGirl与它们一起使用.为什么?因为FactoryGirl不对它们做任何事情.它完全没有意识到你有哪个库.
请记住,您将FactoryGirl语法添加到您选择的测试库中.您不要将库添加到FactoryGirl.
因此,如果FactoryGirl没有使用您首选的库,它在做什么?
在我们得到的引擎盖下的细节,我们需要定义什么 一个 "存根" 是 和其预期的目的:
存根提供了在测试期间进行的调用的固定答案,通常不会对测试中编程的任何内容做出任何响应.存根还可以记录有关呼叫的信息,例如记住它'发送'的消息的电子邮件网关存根,或者可能只记录它'发送'的消息.
这与"模拟"略有不同:
模拟 ......:预编程的对象具有预期,形成了预期接收的调用的规范.
存根可作为使用预设响应设置协作者的方法.仅针对特定测试所触及的协作者公共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
Run Code Online (Sandbox Code Playgroud)
由于FactoryGirl在它们的"存根"方面完全与库无关,因此这是他们采用的方法.
查看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检查它的状态以决定是否应该
保持持久性最新.由于设置的存根定义new_record?
任何has_many
将触发数据库操作.
def new_record?
id.nil?
end
Run Code Online (Sandbox Code Playgroud)
在我抛弃一些修复之前,我想回到一个定义stub
:
存根提供了在测试期间进行的调用的固定答案,通常不会对测试中编程的任何内容做出任何响应.存根还可以记录有关呼叫的信息,例如记住它'发送'的消息的电子邮件网关存根,或者可能只记录它'发送'的消息.
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
Run Code Online (Sandbox Code Playgroud)
FactoryGirl可以在您真正需要时轻松创建100个模型对象1.当然,这是一个负责任的使用问题; 一如既往的强大力量来创造责任.很容易忽略深层嵌套的关联,它们实际上并不属于存根.
另外,正如您所注意到的,FactoryGirl的"存根"抽象有点漏洞,迫使您了解其实现和数据库持久层的内部.使用stubbing 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)
Run Code Online (Sandbox Code Playgroud)
是的,您必须手动设置关联.虽然您只设置了测试/规范所需的那些关联.你没有得到你不需要的其他5个.
这是一个有真正的存根lib有助于明确清楚的事情.这是您的测试/规格,为您提供有关您的设计选择的反馈.通过这样的设置,规范的读者可以问一个问题:"为什么我们需要5个项目?" 如果它对规范很重要,那就很好,它就在前面而且显而易见.否则,它不应该在那里.
对于那些称为单个对象的长链方法或后续对象上的一系列方法,同样的事情也是如此,它可能是时候停止了.该 得墨忒耳定律就是为了帮你,而不是阻碍你.
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
Run Code Online (Sandbox Code Playgroud)
我们永远不会有一个返回id
AND 的存根设置一个has_many
关联.new_record?
FactoryGirl设置的定义完全阻止了这一点.
new_record?
在这里,我们将id
存根与a
的概念分开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
Run Code Online (Sandbox Code Playgroud)
我们仍然需要为每个模型手动添加它.
Bry*_*yce 11
我已经看到这个答案浮出水面,但遇到了同样的问题: FactoryGirl:填充一个有很多关系保留构建策略
我发现最干净的方法是明确地删除关联调用.
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
Run Code Online (Sandbox Code Playgroud)
希望有所帮助!
归档时间: |
|
查看次数: |
14356 次 |
最近记录: |