ruby - RSPEC Let 与创建昂贵对象的实例

标签 ruby rspec mocking factory-bot let

在 RSPEC 中,Let 的行为是内存单个示例(it block),但在某些情况下,就时间而言,这可能会导致一些潜在的不良副作用。

我注意到,如果您设法尝试创建任何被认为是昂贵的东西,例如大型模拟,则整个对象创建过程将在每个调用它的示例中重复。

解决此问题的第一步是将模拟数据缩小到一定大小,这将大部分运行时间从约 30 秒减少到约 .08 秒。鉴于此,通过将一个被调用 3 次以上而没有任何形式的突变的 let 变量转移到一个实例,速度可以提高更多(在这种情况下为 -0.02 到 -0.04)。

通常情况下,惰性评估是可取的,并且在某些情况下此类事情是安全的代价。在大型测试套件(超过 3000 个测试)的上下文中,即使 0.01-0.02 秒的差异经常足以导致 20-30 秒的膨胀。当然,在某些情况下这是任意编号,但您会明白为什么这是不受欢迎的并会产生复杂的问题。

我的问题是:

  • 在什么情况下 let 不再是可行的选择?
  • 有什么方法可以在 block 上下文而不是示例上下文中扩展它的内存? ...或者这是一个可怕的想法?
  • 是否有有效的方法来生成不太可能需要 7 秒以上加载时间的大量模拟数据?我看到过对 Factory Girl 的含糊提及,但在这一点上有足够多的争论,我不知道在当前情况下该作何感想。

感谢您的宝贵时间!

最佳答案

您似乎已经意识到,let 基本上只是阻止您评估变量,除非在您使用它的示例中。如果你在十个例子中使用它,你确实会得到十次昂贵的操作。

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

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

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

FactoryGirl 并不能真正改变你的问题。它将构建并可选地保存对象,但您仍然必须决定在何处以及如何使用它。如果您开始将它放入 before(:each) block 中,或者在大多数示例中调用构建器,您仍然会遇到性能问题。

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

如果您的数据主要在 ActiveRecord 对象中,并且您不热衷于通过 stub /模拟来避免访问数据库,那么您很可能会遇到缓慢的测试。 Fixtures 可以与事务一起使用以提供一些帮助,并且可以比工厂更快,但是根据您的数据库模式、关系等,维护起来可能会很痛苦。我相信您可以在 before(:suite ) block ,然后事务将仍然有效,但这不一定比固定装置更容易维护。

如果您的数据只是占用大量 CPU 资源的对象而不是数据库记录,您可以设置一堆对象并通过 Marshal 序列化它们。模块。然后你可以将它们加载到 let block 中,预先构建并准备就绪,只需点击磁盘(或内存,如果你将 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),但您明白了 - 您正在构建一个副本而不是重新计算任何昂贵的东西伤害你。


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

关于ruby - RSPEC Let 与创建昂贵对象的实例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20792066/

相关文章:

ruby - Watir 更改 highline 的 "ask"方法

ruby-on-rails - "Rake spec"大多数测试失败,但 "rails s"在 Diaspora 源上工作正常

ruby-on-rails - rspec 升级到 3.1.0 后,与 allowed_any_instance_of 一起使用的 and_call_original 不起作用

java - mockito:如何验证对实际实现的调用?

unit-testing - 模拟引发模拟域类的异常

Ruby:从文件实例化对象

ruby - 使用 initialize 定义带参数的类方法

ruby-on-rails - 了解如何使用 Pundit

ruby-on-rails-3 - 如何在 rails 3 中使用 Facebooker2 对 valid_session 进行 rspec ?

java - 在 Spring 上下文中模拟 bean