具有has_many关联的FactoryGirl build_stubbed策略 [英] FactoryGirl build_stubbed strategy with a has_many association

查看:97
本文介绍了具有has_many关联的FactoryGirl build_stubbed策略的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Given a standard has_many relationship between two objects. For a simple example, let's go with:

class Order < ActiveRecord::Base
  has_many :line_items
end

class LineItem < ActiveRecord::Base
  belongs_to :order
end

我想做的是生成带有存根订单项列表的存根订单.

What I'd like to do is generate a stubbed order with a list of stubbed line items.

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

The above code does not work because Rails wants to call save on the order when line_items is assigned and FactoryGirl raises an exception: RuntimeError: stubbed models are not allowed to access the database

那么,如何(或有可能)在has_may集合也存在存根的地方生成存根对象?

So how do you (or is it possible) to generate an stubbed object where it's has_may collection is also stubbed?

推荐答案

TL; DR

FactoryGirl会在做出很大假设时尝试提供帮助 创建它的存根"对象.即: 您有一个id,这意味着您不是新记录,因此已经存在!

TL;DR

FactoryGirl tries to be helpful by making a very large assumption when it creates it's "stub" objects. Namely, that: you have an id, which means you are not a new record, and thus are already persisted!

不幸的是,ActiveRecord使用它来决定是否应该 保持最新状态. 因此,存根模型试图将记录持久保存到数据库中.

Unfortunately, ActiveRecord uses this to decide if it should keep persistence up to date. So the stubbed model attempts to persist the records to the database.

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

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模拟仅应在规范的某些部分使用 生命周期.将他们搬到工厂建立了一个环境, 隐藏违反设计的行为.由此导致的错误将是 令人困惑且难以追踪.

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.

如果您查看有关将RSpec纳入文档的说明 测试/单元, 您会看到它提供了确保模拟正确的方法 在测试之间进行设置和拆除.将模拟物放入工厂 没有提供这样的保证.

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创建存根;使用存根库 (rspec-mocks,minitest/mocks,mocha,flexmock,rr等)

  • Don't use FactoryGirl for creating your stubs; use a stubbing library (rspec-mocks, minitest/mocks, mocha, flexmock, rr, or etc)

如果要将模型属性逻辑保留在FactoryGirl中,那很好. 为此,请使用它并在其他位置创建存根:

If you want to keep your model attribute logic in FactoryGirl that's fine. Use it for that purpose and create the stub elsewhere:

stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
  double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)

是的,您必须手动创建关联.这不是一件坏事, 请参阅下面的进一步讨论.

Yes, you do have to manually create the associations. This is not a bad thing, see below for further discussion.

清除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_manyFactoryGirl关联.这往往导致更紧密的代码耦合 并可能不必要地创建许多嵌套对象.

    IMO, it's generally not a good idea to attempt to create a "stubbed" has_many association with FactoryGirl. This tends to lead to more tightly coupled code and potentially many nested objects being needlessly created.

    要了解此职位以及FactoryGirl的状况,我们需要 看一下几件事:

    To understand this position, and what is going on with FactoryGirl, we need to take a look at a few things:

    • 数据库持久层/gem(即ActiveRecordMongoidDataMapperROM等)
    • 任何存根/模拟库(mintest/mocks,rspec,mocha等)
    • 目的模拟/存根服务
    • The database persistence layer / gem (i.e. ActiveRecord, Mongoid, DataMapper, ROM, etc)
    • Any stubbing / mocking libraries (mintest/mocks, rspec, mocha, etc)
    • The purpose mocks / stubs serve

    每个数据库持久层的行为都不同.实际上,许多行为 在主要版本之间有所不同. FactoryGirl 尝试不做假设 有关如何设置该层的信息.这给了他们最大的灵活性 长途跋涉.

    Each database persistence layer behaves differently. In fact, many behave differently between major versions. FactoryGirl tries to not make assumptions about how that layer is setup. This gives them the most flexibility over the long haul.

    假设:我猜想您在其余部分中使用ActiveRecord 这个讨论.

    Assumption: I'm guessing you are using ActiveRecord for the remainder of this discussion.

    在撰写本文时,ActiveRecord的当前GA版本为4.1.0.什么时候 您在其上设置了has_many关联, a 很多 .

    As of my writing this, the current GA version of ActiveRecord is 4.1.0. When you setup a has_many association on it, there's a lot that goes on.

    这在旧版AR版本中也略有不同.这是非常不同的 Mongoid等.期望FactoryGirl理解 所有这些宝石的错综复杂,也不存在版本之间的差异.就是这样 发生 has_many协会的作家 尝试保持持久性到目前为止.

    This is also slightly different in older AR versions. It's very different in Mongoid, etc. It's not reasonable to expect FactoryGirl to understand the intricacies of all of these gems, nor differences between versions. It just so happens that the has_many association's writer attempts to keep persistence up to date.

    您可能在想:但是我可以用存根设置反函数"

    You may be thinking: "but I can set the inverse with a stub"

    FactoryGirl.define do
      factory :line_item do
        association :order, factory: :order, strategy: :stub
      end
    end
    
    li = build_stubbed(:line_item)
    

    是的,是的.尽管仅仅是因为AR决定 坚持者 . 事实证明,这种行为是一件好事.否则,那将是非常 很难设置临时对象而不频繁访问数据库. 此外,它允许将多个对象保存在单个对象中 交易,如果有问题,则回滚整个交易.

    Yep, that's true. Though it's simply because AR decided not to persist. It turns out this behavior is a good thing. Otherwise, it would be very difficult to setup temp objects without hitting the database frequently. Additionally, it allows for multiple objects to be saved in a single transaction, rolling back the whole transaction if there was a problem.

    现在,您可能会想:我完全可以将对象添加到has_many 击数据库"

    Now, you may be thinking: "I totally can add objects to a has_many without hitting the database"

    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 . 这些依次考虑了基础模型实例的当前状态 决定是现在还是以后坚持.

    Yep, but here order.line_items is really an ActiveRecord::Associations::CollectionProxy. It defines it's own build, #<<, and #concat methods. Of, course these really all delegate back to the association defined, which for has_many are the equivalent methods: ActiveRecord::Associations::CollectionAssocation#build and ActiveRecord::Associations::CollectionAssocation#concat. These take into account the current state of the base model instance in order to decide whether to persist now or later.

    在这里,FactoryFactory真正能做的就是让底层类的行为 定义应该发生什么.实际上,这使您可以使用FactoryGirl来 生成任何类,而不是 只是数据库模型.

    All FactoryGirl can really do here is let the behavior of the underlying class define what should happen. In fact, this lets you use FactoryGirl to generate any class, not just database models.

    FactoryGirl确实尝试在保存对象方面有所帮助.这主要是 在工厂的create一侧.根据他们的维基页面 与ActiveRecord交互:

    FactoryGirl does attempt to help a little with saving objects. This is mostly on the create side of the factories. Per their wiki page on interaction with ActiveRecord:

    ... [工厂]首先保存关联,以便正确使用外键 根据相关模型进行设置.要创建一个实例,它不带任何新调用 参数,分配每个属性(包括关联),然后调用 救!. factory_girl对创建ActiveRecord并没有做任何特殊的事情 实例.它不会与数据库交互或扩展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.

    等等!您可能已经注意到,在上面的示例中,我遗漏了以下内容:

    Wait! You may have noticed, in the example above I slipped the following:

    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=设置为数组,而不是 坚持!那有什么呢?

    Yep, that's right. We can set order.line_items= to an array and it isn't persisted! So what gives?

    有许多不同的类型,FactoryGirl可以与它们一起使用.为什么? 因为FactoryGirl对它们中的任何一个都不做任何事情.完全是 不知道您拥有哪个图书馆.

    There are many different types and FactoryGirl works with them all. Why? Because FactoryGirl doesn't do anything with any of them. It's completely unaware of which library you have.

    请记住,您将FactoryGirl语法添加到选择的测试库. 您没有将库添加到FactoryGirl.

    Remember, you add the FactoryGirl syntax to your test library of choice. You don't add your library to FactoryGirl.

    因此,如果FactoryGirl不使用您喜欢的库,那它在做什么?

    So if FactoryGirl isn't using your preferred library, what is it doing?

    在深入了解细节之前,我们需要定义什么 a 存根" 及其预期用途:

    Before we get to the under the hood details, we need to define what a "stub" is and its 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'.

    这与模拟"有细微的区别:

    this is subtly different from a "mock":

    模拟 ...:根据期望进行预编程的对象,它们构成了一个 他们希望收到的电话的规格.

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

    存根是一种使用罐头响应设置协作者的方法.坚持 只有您为特定测试触摸的协作者公共API会保留 存根轻巧小.

    Stubs serve as a way to setup collaborators with canned responses. Sticking to only the collaborators public API which you touch for the specific test keeps stubs lightweight and small.

    没有任何存根"库,您可以轻松创建自己的存根:

    Without any "stubbing" library, you can easily create your own stubs:

    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完全与图书馆无关 存根",这是他们采用的方法.

    Since FactoryGirl is completely library agnostic when it comes to their "stubs", this is the approach they take.

    查看FactoryGirl v.4.4.0的实现,我们可以看到 build_stubbed时,以下方法都是存根的:

    Looking at the FactoryGirl v.4.4.0 implementation, we can see that the following methods are all stubbed when you build_stubbed:

    • persisted?
    • new_record?
    • save
    • destroy
    • connection
    • reload
    • update_attribute
    • update_column
    • created_at
    • persisted?
    • new_record?
    • save
    • destroy
    • connection
    • reload
    • update_attribute
    • update_column
    • created_at

    这些都是非常ActiveRecord-y的.但是,正如您在has_many中看到的那样, 这是一个相当泄漏的抽象. ActiveRecord公共API表面积为 很大.期望图书馆完全覆盖它是完全不合理的.

    These are all very ActiveRecord-y. However, as you have seen with has_many, it is a fairly leaky abstraction. The ActiveRecord public API surface area is very large. It's not exactly reasonable to expect a library to fully cover it.

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

    Why does the has_many association not work with the FactoryGirl stub?

    如上所述,ActiveRecord检查其状态以决定是否应 保持最新状态. 由于new_record? 设置任何has_many都会触发数据库操作.

    As noted above, ActiveRecord checks it's state to decide if it should keep persistence up to date. Due to the stubbed definition of new_record? setting any has_many will trigger a database action.

    def new_record?
      id.nil?
    end
    

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

    Before I throw out some fixes, I want to go back to the definition of a 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'.

    存根的FactoryGirl实现违反了此原则.由于没有 知道您在测试/规格中将要做什么,它只是试图 阻止数据库访问.

    The FactoryGirl implementation of a stub violates this tenet. Since it has no idea what you are going to be doing in your test/spec, it simply tries to prevent database access.

    如果要创建/使用存根,请使用专用于该任务的库.自从 似乎您已经在使用RSpec,请使用它的double功能(以及新的 instance_double class_double , 以及 object_double 在RSpec 3中).或者 使用Mocha,Flexmock,RR或其他任何工具.

    If you wish to create / use stubs, use a library dedicated to that task. Since it seems you are already using RSpec, use it's double feature (and the new verifying instance_double, class_double, as well as object_double in RSpec 3). Or use Mocha, Flexmock, RR, or anything else.

    您甚至可以滚动自己的超级简单存根工厂(是的, 这仅仅是制作罐头物体的简单方法的一个例子 回复):

    You can even roll your own super simple stub factory (yes there are issues with this, it's simply an example of an easy way to make an object with canned responses):

    require 'ostruct'
    def create_stub(stubbed_attributes)
      OpenStruct.new(stubbed_attributes)
    end
    

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

    FactoryGirl makes it very easy to create 100 model objects when really you needed 1. Sure, this is a responsible usage issue; as always great power comes create responsibility. It's just very easy to overlook deeply nested associations, which don't really belong in a stub.

    此外,您已经注意到,FactoryGirl的存根"抽象有点 泄漏迫使您同时了解其实现和数据库 持久层的内部.使用存根库可以完全释放您的精力 具有这种依赖性.

    Additionally, as you have noticed, FactoryGirl's "stub" abstraction is a bit leaky forcing you to understand both its implementation and your database persistence layer's internals. Using a stubbing lib should completely free you from having this dependency.

    如果要将模型属性逻辑保留在FactoryGirl中,那很好. 为此,请使用它并在其他位置创建存根:

    If you want to keep your model attribute logic in FactoryGirl that's fine. Use it for that purpose and create the stub elsewhere:

    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个 那些你不需要的.

    Yes, you do have to manually setup the associations. Though you only setup those associations which you need for the test/spec. You don't get the 5 other ones that you do not need.

    这是一件事,拥有一个真正的存根库有助于清楚地说明. 这是您的测试/规格,可为您提供有关设计选择的反馈.用 这样的设置,规范的读者可以提出以下问题:为什么我们需要5 订单项?" 如果这对规范很重要,那很好就在前面 很明显否则,它不应该在那里.

    This is one thing that having a real stubbing lib helps make explicitly clear. This is your tests/specs giving you feedback on your design choices. With a setup like this, a reader of the spec can ask the question: "Why do we need 5 line items?" If it's important to the spec, great it's right there up front and obvious. Otherwise, it shouldn't be there.

    对于称为单个对象的一长串方法来说,同样的事情, 或后续对象上的方法链,可能是时候停止了.这 迪米特法则可以为您提供帮助 你,而不是阻碍你.

    The same thing goes for those a long chain of methods called a single object, or a chain of methods on subsequent objects, it's probably time to stop. The law of demeter is there to help you, not hinder you.

    这更像是骇客.我们知道默认存根设置了id.因此,我们 只需将其删除即可.

    This is more of a hack. We know that the default stub sets an id. Thus, we simply remove it.

    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?的定义 防止这种情况.

    We can never have a stub which returns an id AND sets up a has_many association. The definition of new_record? that FactoryGirl setup completely prevents this.

    在这里,我们将id的概念与存根是 new_record?.我们将其推送到模块中,以便可以在其他地方重复使用.

    Here, we separate the concept of an id from where the stub is a new_record?. We push this into a module so we can re-use it in other places.

    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
    

    我们仍然必须为每个模型手动添加它.

    We still have to manually add it for each model.

    这篇关于具有has_many关联的FactoryGirl build_stubbed策略的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆