gpt4 book ai didi

ruby-on-rails - 具有 has_many 关联的 FactoryGirl build_stubbed 策略

转载 作者:行者123 更新时间:2023-12-03 07:42:28 26 4
gpt4 key购买 nike

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

class Order < ActiveRecord::Base
has_many :line_items
end

class LineItem < ActiveRecord::Base
belongs_to :order
end

我想要做的是生成一个 stub 订单,其中包含一个 stub 订单项列表。
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 时调用订单的 save 并且 FactoryGirl 引发异常: RuntimeError: stubbed models are not allowed to access the database
那么你如何(或者是否有可能)生成一个 stub 对象,它的 has_may 集合也被 stub ?

最佳答案

TL; 博士

FactoryGirl 试图通过做出非常大的假设来提供帮助
创建它的“ stub ”对象。即:
you have an id , which means you are not a new record, and thus are already persisted!

不幸的是,ActiveRecord 使用它来决定它是否应该
keep persistence up to date
因此, stub 模型尝试将记录持久化到数据库中。

Please do not try to shim RSpec stubs / mocks into FactoryGirl factories. Doing so mixes two different stubbing philosophies on the same object. Pick one or the other.

RSpec mocks are only supposed to be used during certain parts of the spec life cycle. Moving them into the factory sets up an environment which will hide the violation of the design. Errors which result from this will be confusing and difficult to track down.

If you look at the documentation for including RSpec into say test/unit, you can see that it provides methods for ensuring that the mocks are properly setup and torn down between the tests. Putting the mocks into the factories provides no such guarantee that this will take place.



这里有几个选项:
  • 不要使用 FactoryGirl 创建您的 stub ;使用 stub 库
    (rspec-mocks、minitest/mocks、mocha、flexmock、rr 等)

    如果您想在 FactoryGirl 中保留模型属性逻辑,那很好。
    为此目的使用它并在其他地方创建 stub :
    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,尝试创建“ stub ” has_many 通常不是一个好主意
    FactoryGirl 的关联。这往往会导致更紧密耦合的代码
    并且可能会不必要地创建许多嵌套对象。

    要了解这个位置,以及 FactoryGirl 的情况,我们需要
    看看几件事:
  • 数据库持久层/gem(即 ActiveRecord , Mongoid ,DataMapperROM 等)
  • 任何 stub /模拟库(mintest/mocks、rspec、mocha 等)
  • 目的模拟/ stub 服务

  • 数据库持久层

    每个数据库持久层的行为都不同。事实上,许多行为
    主要版本之间有所不同。 FactoryGirl 尽量不做假设
    关于该层是如何设置的。这为他们提供了最大的灵活性
    长途。

    假设: 我猜你在使用 ActiveRecord这个讨论。

    在我撰写本文时, ActiveRecord 的当前 GA 版本是 4.1.0。什么时候
    你在上面设置了一个 has_many 关联,
    there's
    a
    lot
    that
    goes
    on

    这在较旧的 AR 版本中也略有不同。这是非常不同的
    Mongoid 等。 期望 FactoryGirl 理解是不合理的
    所有这些 gem 的错综复杂,也没有版本之间的差异。就这样
    碰巧 has_many association's writer
    尝试 keep persistence up to date

    你可能会想:“但我可以用 stub 设置逆”
    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
    方法。当然,这些真的都委托(delegate)回定义的关联,
    其中 has_many 是等效的方法:
    ActiveRecord::Associations::CollectionAssocation#build
    ActiveRecord::Associations::CollectionAssocation#concat
    这些按顺序考虑了基础模型实例的当前状态
    决定是现在坚持还是以后坚持。

    FactoryGirl 在这里真正能做的就是让底层类的行为
    定义应该发生什么。事实上,这让您可以使用 FactoryGirl
    generate any class ,不是
    只是数据库模型。

    FactoryGirl 确实试图帮助保存对象。这主要是
    在工厂的 create 一侧。根据他们的维基页面
    interaction with ActiveRecord:

    ...[a factory] saves associations first so that foreign keys will be properly set on dependent models. To create an instance, it calls new without any arguments, assigns each attribute (including associations), and then calls save!. factory_girl doesn’t do anything special to create ActiveRecord instances. It doesn’t interact with the database or extend ActiveRecord or your models in any way.



    等待!您可能已经注意到,在上面的示例中,我忽略了以下内容:
    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= 设置为一个数组,但它不是
    坚持了!那么什么给呢?

    stub /模拟库

    有许多不同的类型,FactoryGirl 可以与它​​们一起工作。为什么?
    因为 FactoryGirl 不会对它们中的任何一个做任何事情。完全是
    不知道你有哪个图书馆。

    请记住,您将 FactoryGirl 语法添加到 test library of choice 中。
    您不会将库添加到 FactoryGirl。

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

    目的模拟/ stub 服务

    在我们深入细节之前,我们需要定义 what
    a
    "stub"
    is
    及其 intended purpose :

    Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'.



    这与“模拟”略有不同:

    Mocks...: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.



    stub 用作设置具有预设响应的协作者的一种方式。坚持
    只有您为特定测试触摸的合作者公共(public) API 会保留
    stub 轻巧小巧。

    没有任何“ stub ”库,您可以轻松创建自己的 stub :
    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 完全与图书馆无关
    “ stub ”,这是 the approach they take

    查看 FactoryGirl v.4.4.0 实现,我们可以看到
    当您 build_stubbed 时,以下方法都被 stub :
  • persisted?
  • new_record?
  • save
  • destroy
  • connection
  • reload
  • update_attribute
  • update_column
  • created_at

  • 这些都是非常 ActiveRecord 的。但是,正如您在 has_many 中看到的那样,
    这是一个相当有漏洞的抽象。 ActiveRecord 公共(public) API 表面区域是
    很大。期望图书馆完全覆盖它是不完全合理的。

    为什么 has_many 关联不适用于 FactoryGirl stub ?

    如上所述,ActiveRecord 检查它的状态以决定它是否应该
    keep persistence up to date
    由于 stubbed definition of new_record?
    设置任何 has_many 将触发数据库操作。
    def new_record?
    id.nil?
    end

    在我抛出一些修复之前,我想回到 stub 的定义:

    Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'.



    stub 的 FactoryGirl 实现违反了这一原则。由于它没有
    知道你将在你的测试/规范中做什么,它只是试图
    防止数据库访问。

    修复 #1:不要使用 FactoryGirl 创建 stub

    如果您希望创建/使用 stub ,请使用专用于该任务的库。自从
    看来您已经在使用 RSpec,使用它的 double 功能(以及新的验证
    instance_double ,
    class_double ,
    以及 object_double
    在 RSpec 3)。要么
    使用 Mocha、Flexmock、RR 或其他任何东西。

    你甚至可以推出你自己的 super 简单的 stub 工厂(是的,有问题
    这只是一个简单的方法来制作带有 jar 头的物体的例子
    回复):
    require 'ostruct'
    def create_stub(stubbed_attributes)
    OpenStruct.new(stubbed_attributes)
    end

    FactoryGirl 使创建 100 个模型对象变得非常容易
    需要 1. 当然,这是一个负责任的使用问题;一如既往,强大的力量降临
    创造责任。很容易忽略深层嵌套
    关联,它们并不真正属于 stub 。

    此外,正如您所注意到的,FactoryGirl 的“ stub ”抽象有点
    泄漏迫使您了解其实现和您的数据库
    持久层的内部结构。使用 stub 库应该完全释放你
    从有这种依赖性。

    如果您想在 FactoryGirl 中保留您的模型属性逻辑,那很好。
    为此目的使用它并在其他地方创建 stub :
    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 个
    那些你不需要的。

    拥有真正的 stub 库有助于明确说明这一点。
    这是您的测试/规范,为您提供有关设计选择的反馈。用一个
    像这样设置,规范的读者可以问这样的问题:“为什么我们需要 5
    订单项?”如果它对规范很重要,那么很好,它就在前面
    并且很明显。否则,它不应该在那里。

    同样的事情也适用于那些称为单个对象的长链方法,
    或者后续对象上的一系列方法,可能是时候停止了。这
    law of demeter 有帮助
    你,不妨碍你。

    修复 #2:清除 id 字段

    这更像是一种黑客攻击。我们知道默认 stub 设置了一个 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 的 stub
    协会。 FactoryGirl 完全设置的 new_record? 的定义
    防止这种情况。

    修复 #3:创建您自己的 new_record? 定义

    在这里,我们将 id 的概念与 stub 是一个 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

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

    关于ruby-on-rails - 具有 has_many 关联的 FactoryGirl build_stubbed 策略,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17754770/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com