RSPEC Let vs Instance具有昂贵的对象创建

baw*_*ver 5 ruby rspec mocking let factory-bot

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

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

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

通常,可以认为延迟评估是可取的,并且在某些情况下,这样做是安全的代价。在大型测试套件(3000多个测试)的情况下,甚至0.01-0.02秒的差异通常足以导致20-30秒的膨胀。当然,在某些情况下,这是任意编号,但是您可以看到为什么这是不希望的,并产生复合问题。

我的问题是:

  • 在什么情况下不再可行?
  • 有什么办法可以在块上下文而不是示例上下文中扩展其记忆吗?...或者这是一个可怕的想法?
  • 是否有有效的方法来生成大量的模拟数据,而这些模拟数据的加载时间可能不会超过7秒?我看到过模糊的提及《工厂女郎》,但在该笔记上有足够的争论,我不知道在当前情况下该怎么想。

感谢您的时间!

Ner*_*ter 4

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

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

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

我认为你的第三个问题是真正的内容在哪里,所以请耐心听我说。

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

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

如果您的数据主要位于 ActiveRecord 对象中,并且您不热衷于存根/模拟来避免访问数据库,那么您很可能会陷入缓慢的测试。固定装置可以与事务一起使用来提供一点帮助,并且可以比工厂更快,但是根据您的数据库模式、关系等,维护可能会很痛苦。我相信您可以在块中使用工厂,然后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")) }
Run Code Online (Sandbox Code Playgroud)

这样做的优点是可以完整序列化对象的状态,并将其完全恢复为原样,而无需重新计算昂贵的数据。对于像 ActiveRecord 模型这样非常复杂的对象,这可能不太有效,但对于您自己设计的更简单的数据结构来说,它可以很方便。marshal_dump您甚至可以通过实现和方法来实现您自己的转储/加载逻辑marshal_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 }
Run Code Online (Sandbox Code Playgroud)

这并不一定适用于所有情况,dup浅拷贝也是如此(请参阅Ruby 文档以获取更多信息),但您明白了 - 您正在构建一个副本,而不是重新计算任何对您造成伤害的昂贵内容。


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

  • 说得好。我想补充一点,当测试一个对象很痛苦时,通常是因为该对象太复杂了。任何测试框架或方法都没有多大帮助;只有将其分解成更小的碎片才能减轻疼痛。 (2认同)
  • @zetetic:非常正确,尽管这是一个完全不同的主题,而且是一个巨大的主题。请记住,存在需要测试复杂对象的合法情况 - 例如,您想要自信地重构遗留应用程序。当然,Rails 倾向于仅通过其堆栈的工作方式来引导新的 Ruby 开发人员走向紧密耦合,这无济于事。改变了你的架构?去修复你的 HTML、控制器的参数白名单,也许还有你的模型,……这是破坏新开发人员对架构理解的绝妙方法。 (2认同)