RSPEC让vs实例创建昂贵的对象

时间:2013-12-26 22:25:23

标签: ruby rspec mocking factory-bot let

在RSPEC中,Let的行为是在单个示例(它阻止)中进行记忆,但在某些情况下,这可能会导致一些潜在的令人讨厌的副作用。

我注意到如果你设法尝试创建任何被认为是昂贵的东西,例如大型模拟,那么整个对象创建将重复它所调用的每一个例子。

对此进行故障排除的第一步是将模拟数据破解为大小,这将大部分运行时间从~30秒减少到~0.08秒。通过将一个被调用3次以上且没有任何形式突变的let变量传递给实例,速度可以进一步提高(在这种情况下为-0.02到-0.04)。

通常,可以推测懒惰的评估是可取的,并且在某些情况下这样的事情是安全的代价。在大型测试套件(3000+测试)的背景下,甚至0.01-0.02秒的差异通常足以导致20-30秒的膨胀。当然,在某些情况下,这是任意编号,但你可以看出为什么这是不合需要的并且会产生复合问题。

我的问题是:

  • 在哪些情况下不再是一个可行的选择?
  • 有没有办法在块上下文中扩展其'memoization而不是示例上下文? ......或者这是一个可怕的想法?
  • 是否有有效的方法来生成非常大量的模拟数据,这些数据不太可能需要7秒多的时间才能加载?我已经看到了对Factory Girl的模糊引用,但是在那个音符上有足够的战斗我不知道在当前的背景下该怎么想。

感谢您的时间!

1 个答案:

答案 0 :(得分:3)

您似乎意识到,let基本上只是让您无法评估变量,除非您使用它的示例。如果您在十个示例中使用它,那么您确实会在昂贵的操作中获得十次点击。

因此,对于您的第一个问题,我不知道我能提供有用的答案。这是非常情境化的,但是如果你经常使用变量并且这是一个昂贵的操作,我会说let是不可行的。但根据您的需求,它可能仍然是最佳选择 - 也许您必须在大多数示例中重置状态,但不是全部。在这种情况下,操作的费用可能不值得尝试在少数情况下分享它。

对于你的第二个问题,我想说尝试让let在一个区块内工作可能不是一个好主意。这是before(:all)块和实例变量的情况。

你的第三个问题是我认为真正的肉在哪里,所以请耐心等待。

FactoryGirl实际上不会改变你的问题。它将构建并可选地保存对象,但您仍需要决定在何处以及如何使用它。如果您开始将其弹出到before(:each)块中,或者在大多数示例中调用构建器,您仍然会有性能命中。

根据您的需要,您可以在before(:all)块甚至before(:suite)块中执行昂贵的操作(例如,在spec_helper.rb中配置)。这样做的好处是可以减少对昂贵操作的点击次数,但缺点是如果您正在修改数据,则会对所有其他测试进行修改。这显然会导致许多难以调试的问题。如果您的数据需要通过多个示例进行更改,然后重置为原始状态,那么您将遇到某种性能损失或自己设计的自定义逻辑。

如果您的数据主要位于ActiveRecord对象中,并且您并不热衷于存根/模拟以防止访问数据库,那么您可能会遇到缓慢的测试问题。 Fixtures可以与事务一起使用,并且可以比工厂更快,但根据您的数据库架构,关系等可能很难维护。我相信您可以在before(:suite)块中使用工厂,然后交易仍然有效,但这并不一定比固定装置更容易维护。

如果您的数据只是CPU资源而不是数据库记录,那么您可以设置一堆对象并通过Marshal模块对它们进行序列化。然后你可以在一个let块中加载它们,预先建好并准备就绪,只需要一个磁盘命中(或者内存,如果你将Marshalled字符串存储在内存中):

# In irb or pry or even spec_helper.rb
object = SomeComplexThing.new
object.prepare_it_with_expensive_method_call_fun
Marshal.dump(object) # Store the output of this somewhere

# In some_spec.rb
let(:thing) { Marshal.load(IO.read("serialized_thing")) }

这样做的好处是可以完全序列化对象的状态,并在不重新计算昂贵数据的情况下完全恢复它。对于像ActiveRecord模型这样的非常复杂的对象,这可能不会有效,但它对于您自己设计的更简单的数据结构非常方便。您甚至可以通过实施marshal_dumpmarshal_load方法来实现自己的转储/加载逻辑(请参阅我上面链接的Marshal文档),这可以在测试之外使用。

如果您的数据足够简单,您甚至可以使用以下设置:

# In spec_helper.rb
RSpec.configure do |config|
  config.before(:suite) do
    @object = SomeComplexThing.new
    @object.prepare_it_with_expensive_method_call_fun
  end
end

# In a test
let(:thing) { @object.dup }

这并不一定适用于所有情况,因为dup是一个浅层副本(有关详细信息,请参阅the Ruby docs),但您明白了 - 您正在构建副本而不是而不是重新计算任何昂贵的东西会伤害你。


我希望这些信息有所帮助,因为我不确定我是否完全理解你需要的信息。