Ruby on rails FactoryGirl建立了一个多关联的存根策略

Ruby on rails FactoryGirl建立了一个多关联的存根策略,ruby-on-rails,factory-bot,Ruby On Rails,Factory Bot,给定一个标准,两个对象之间有许多关系。举个简单的例子,让我们来看一下: class Order < ActiveRecord::Base has_many :line_items end class LineItem < ActiveRecord::Base belongs_to :order end 上述代码不起作用,因为Rails希望在分配了第_行项目且FactoryGirl引发异常时调用订单上的save: 运行时错误:不允许存根模型访问数据库 那么,如何(或是否可能

给定一个标准,两个对象之间有许多关系。举个简单的例子,让我们来看一下:

class Order < ActiveRecord::Base
  has_many :line_items
end

class LineItem < ActiveRecord::Base
  belongs_to :order
end
上述代码不起作用,因为Rails希望在分配了第_行项目且FactoryGirl引发异常时调用订单上的save:
运行时错误:不允许存根模型访问数据库


那么,如何(或是否可能)在其has_may集合也是存根的情况下生成存根对象呢?

我看到了这个答案,但遇到了与您相同的问题:

我发现的最干净的方法是显式地剔除关联调用

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

希望有帮助

我发现Bryce的解决方案是最优雅的,但它对新的
allow()
语法产生了一个弃用警告

为了使用新的(更干净的)语法,我做了以下操作:

2014年5月6日更新:我的第一个建议是使用私有api方法,多亏Aaraon K提供了更好的解决方案,请阅读评论以便进一步讨论

TL;博士 FactoryGirl试图通过做一个非常大的假设来提供帮助 创建它的“存根”对象。即:

不幸的是,ActiveRecord使用它来决定是否应该这样做 . 因此存根模型尝试将记录持久化到数据库中

请不要尝试将RSpec存根/模拟填充到FactoryGirl工厂中。 这样做会在同一个对象上混合两种不同的存根哲学。挑 一个或另一个

RSpec模拟只应在规范的某些部分使用 生命周期把他们搬进工厂会创造一个 隐藏违反设计的行为。由此产生的错误将是 令人困惑且难以追踪

如果您查看将RSpec包括在say中的文档 , 您可以看到,它提供了一些方法来确保模拟正确地进行 设置并在测试之间拆除。把模型放进工厂 不提供此类保证,以确保这将发生

这里有几个选项:

  • 不要使用FactoryGirl创建存根;使用存根库 (rspec模拟、小型测试/模拟、摩卡、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)
    
    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
    
  • 创建自己的
    新记录定义?

    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
    
这是怎么回事? 在我看来,试图创建一个“存根”的
有很多

与FactoryGirl的关联
。这往往会导致更紧密耦合的代码
并且可能会不必要地创建许多嵌套对象

为了了解这个职位,以及FactoryGirl的现状,我们需要 请看几件事:

  • 数据库持久层/gem(即
    ActiveRecord
    Mongoid
    DataMapper
    ROM
    等)
  • 任何存根/模拟库(mintest/mocks、rspec、mocha等)
  • 模拟/存根的用途
数据库持久层 每个数据库持久性层的行为都不同。事实上,很多人都表现得很好 不同的主要版本。工厂女孩尽量不做假设 关于如何设置该层。这使他们在整个过程中具有最大的灵活性 长途运输

假设:我猜您正在使用
ActiveRecord
来处理剩余的 这是一次讨论

在我写这篇文章时,
ActiveRecord
的当前GA版本是4.1.0。什么时候 您设置了一个有许多关联的

这在较旧的AR版本中也略有不同。这在中国很不一样 Mongoid等。期望FactoryGirl理解 所有这些宝石的复杂性,也没有版本之间的差异。就这样 碰巧 试图

你可能会想:“但我可以用存根设置反向”

没错。虽然这只是因为阿尔决定 . 事实证明,这种行为是一件好事。否则,这将是非常困难的 很难在不频繁访问数据库的情况下设置临时对象。 此外,它允许在一个文件中保存多个对象 事务,如果出现问题,则回滚整个事务

现在,您可能会想:“我完全可以在没有 “点击数据库”

是的,没错。我们可以将
order.line\u items=
设置为数组,但它不是 坚持!那么是什么原因呢

存根/模拟库 有许多不同的类型,FactoryGirl都与之合作。为什么? 因为FactoryGirl对他们都不做任何事。完全是 不知道你有哪个图书馆

请记住,您将FactoryGirl语法添加到了。 您不会将库添加到FactoryGirl

所以,如果FactoryGirl没有使用您喜欢的库,它在做什么

模拟/存根的用途 在我们讨论引擎盖下的细节之前,我们需要定义 及其:

存根为测试期间拨打的电话提供固定答案,通常不提供 对测试程序之外的任何东西都没有反应。 存根还可以记录有关呼叫的信息,例如电子邮件网关存根 它会记住它“发送”的消息,或者只记得它发送了多少消息 “发送”

这与“模拟”有着微妙的区别:

模拟…:使用预期值预先编程的对象,形成模拟 指定他们预期接收的呼叫

存根是一种通过固定的响应来设置协作者的方法。坚持 只有科尔
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
FactoryGirl.define do
  factory :line_item do
    association :order, factory: :order, strategy: :stub
  end
end

li = build_stubbed(:line_item)
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 = 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
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
def new_record?
  id.nil?
end
require 'ostruct'
def create_stub(stubbed_attributes)
  OpenStruct.new(stubbed_attributes)
end
stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
  double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)
after(:stub) do |order, evaluator|
  order.id = nil
  order.line_items = build_stubbed_list(
    :line_item,
    evaluator.line_items_count,
    order: order
  )
end
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