【问题标题】:RSPEC Let vs Instance with expensive object creationRSPEC Let vs Instance 与昂贵的对象创建
【发布时间】:2014-01-14 12:52:24
【问题描述】:

在 RSPEC 中,Let 的行为是对单个示例进行记忆(it 块),但在某些情况下,就时间而言,这可能会导致一些潜在的讨厌的副作用。

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

解决此问题的第一步是将模拟数据缩小到一定大小,这将大部分运行时间从约 30 秒缩短到约 0.08 秒。鉴于此,通过将被调用 3 次以上且没有任何形式的突变的 let 变量传输到实例,可以进一步提高速度(在本例中为 -0.02 到 -0.04)。

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

我的问题是:

  • 在哪些情况下让不再可行?
  • 有什么方法可以将它的记忆延伸到块上下文而不是示例上下文? ...或者这是一个可怕的想法?
  • 是否有有效的方法来生成异常大量的模拟数据,这些数据加载时间可能不会超过 7 秒?我看到对工厂女孩的模糊提及,但在那个音符上有足够的争论,我不知道在当前情况下该怎么想。

感谢您的宝贵时间!

【问题讨论】:

  • 嗨@lemur,你能提供一个代码示例(缩写,因为你的代码库很大)? Let 应该进行惰性评估,因此您所描述的行为不应该发生。 relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
  • 使用 let 定义一个 memoized 辅助方法。该值将在同一示例中的多个调用中缓存但不跨示例这就是问题所在。每个“it”块都是一个例子。不幸的是,我无法在此发布代码,但我会在周末给出类似场景的示例。

标签: ruby rspec mocking factory-bot let


【解决方案1】:

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

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

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

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

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

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

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

如果您的数据只是 CPU 昂贵的对象而不是数据库记录,您可以设置一堆对象并通过 Marshal 模块将它们序列化。然后,您可以将它们加载到 let 块中,预先构建并准备好,只需磁盘命中(或内存,如果您将编组字符串存储在内存中):

# 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),但你明白了 - 你正在构建一个副本而不是重新计算任何昂贵的东西都在伤害你。


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

【讨论】:

  • 说得好。我要补充一点,当测试对象很痛苦时,通常是因为对象太复杂了。任何测试框架或方法都不会有太大帮助。只有把它分成更小的部分才能减轻痛苦。
  • @zetetic:非常正确,尽管这是一个完全不同的话题,而且是一个巨大的话题。请记住,有个合法的案例来测试一个复杂的对象——例如,你希望有信心重构的遗留应用程序。当然,Rails 倾向于仅仅通过其堆栈的工作方式来引导新的 Ruby 开发人员走向紧密耦合并没有帮助。改变了你的架构?去修复你的 HTML,你的控制器的参数白名单,也许你的模型,....破坏新开发人员对架构的理解的绝妙方式。
猜你喜欢
  • 1970-01-01
  • 2016-10-28
  • 2014-01-07
  • 2021-10-06
  • 1970-01-01
  • 1970-01-01
  • 2013-01-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多