【问题标题】:FactoryGirl build_stubbed strategy with a has_many association具有 has_many 关联的 FactoryGirl build_stubbed 策略
【发布时间】:2019-09-14 13:59:22
【问题描述】:

给定两个对象之间的标准 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

上面的代码不起作用,因为当分配了 line_items 并且 FactoryGirl 引发了异常时,Rails 想要在订单上调用 save: RuntimeError: stubbed models are not allowed to access the database

那么你如何(或者有可能)生成一个存根对象,它的 has_may 集合也被存根?

【问题讨论】:

  • 所以你的意思是存根是你不想让它命中数据库吗?你这样做的目的是什么?
  • 怎么样:order.stub(:line_items).and_return build_stubbed_list(...)
  • @apneadiving 我们的目标是在工厂里完成这一切。当然我可以在我的规范(或测试)中存根该方法,但它不是一个很好的单行。
  • 我知道并且我在工厂的 after(:stubs) 块中这样做
  • 您找到解决方案了吗?

标签: ruby-on-rails factory-bot


【解决方案1】:

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(即ActiveRecordMongoidDataMapperROM 等)
  • 任何存根/模拟库(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#&lt;&lt;, 和#concat 方法。当然,这些真的都是委托给协会定义的, has_many 是等效的方法: ActiveRecord::Associations::CollectionAssocation#buildActiveRecord::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

我们仍然需要为每个模型手动添加它。

【讨论】:

  • 惊人的彻底和清晰的答案 w/ TL;DR!如果可以的话,我会给你10票!谢谢!
  • 从来没有真正认为这是一个解决方案,但你确实很好地解释了这种情况亚伦。感谢您将此页面作为每个人的资源!
  • @Aaron K 惊人的帖子,但有些链接没有找到和损坏。你能重新检查一下吗?谢谢
  • 出色的答案。非常有帮助
【解决方案2】:

我看过这个答案,但遇到了同样的问题: 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

希望有帮助!

【讨论】:

  • 这确实有帮助!谢谢。
【解决方案3】:

我发现 Bryce 的解决方案是最优雅的,但它会产生关于新 allow() 语法的弃用警告。

为了使用新的(更简洁的)语法,我这样做了:

2014 年 6 月 5 日更新:我的第一个提议是使用私有 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
...

【讨论】:

  • enable_expect 是私有 API;请不要使用它。 RSpec 模拟只允许在 RSpec 生命周期的特定部分内。这种用法打开了进一步问题的可能性。 FactoryGirl 的存根策略与 RSpec 存根无关。由于它们所代表的共享“存根”概念,它们只是共享相同的名称。
  • 如果您真的想要这种语法,我仍然建议您避免使用,那么执行此操作的公共 API 方法是:FactoryGirl::SyntaxRunner.include(RSpec::Mocks::ExampleMethods)。可以找到这方面的文档:relishapp.com/rspec/rspec-mocks/v/3-0/docs/test-frameworks/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多