何时使用 RSpec let()?

Posted

技术标签:

【中文标题】何时使用 RSpec let()?【英文标题】:When to use RSpec let()? 【发布时间】:2011-07-18 14:09:14 【问题描述】:

我倾向于使用 before 块来设置实例变量。然后我在我的示例中使用这些变量。我最近遇到了let()。根据 RSpec 文档,它用于

... 定义一个记忆化的辅助方法。该值将在同一示例中的多个调用中缓存,但不会跨示例缓存。

这与在 before 块中使用实例变量有何不同?还有什么时候应该使用let() vs before()

【问题讨论】:

让块被延迟评估,而 before 块在每个示例之前运行(它们总体上较慢)。使用 before 块取决于个人喜好(编码风格、模拟/存根......)。 Let 块通常是首选。可以查看更详细的info about let 在 before 钩子中设置实例变量不是一个好习惯。查看betterspecs.org 【参考方案1】:

我总是更喜欢 let 而不是实例变量,原因如下:

实例变量在被引用时立即存在。这意味着,如果您对实例变量的拼写粗心大意,则会创建一个新变量并将其初始化为nil,这可能会导致细微的错误和误报。因为let 创建了一个方法,所以当你拼错它时,你会得到一个NameError,我觉得这更可取。它也使重构规范变得更加容易。 before(:each) 挂钩将在每个示例之前运行,即使该示例不使用挂钩中定义的任何实例变量。这通常没什么大不了的,但是如果实例变量的设置需要很长时间,那么您就是在浪费周期。对于let定义的方法,初始化代码只有在示例调用时才会运行。 您可以将示例中的局部变量直接重构为 let 而无需更改 引用示例中的语法。如果重构为实例变量,则必须更改 您在示例中如何引用对象(例如,添加 @)。 这有点主观,但正如 Mike Lewis 所指出的,我认为它使规范更易于阅读。我喜欢用let 定义所有依赖对象并保持it 块简洁明了的组织方式。

相关链接可以在这里找到:http://www.betterspecs.org/#let

【讨论】:

我真的很喜欢你提到的第一个优势,但你能解释一下第三个优势吗?到目前为止,我看到的示例(mongoid 规范:github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/…)使用单行块,我看不出没有“@”如何使其更易于阅读。 正如我所说,这有点主观,但我发现使用 let 定义所有依赖对象并使用 before(:each) 设置所需的配置或任何模拟/存根很有帮助示例所需要的。我更喜欢这个,而不是一个包含所有这些的大钩子。此外,let(:foo) Foo.new before(:each) @foo = Foo.new 噪音更小(而且更重要)。这是我如何使用它的示例:github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/… 感谢您的示例,这真的很有帮助。 Andrew Grimm:是的,但警告可能会产生大量噪音(即,您使用的宝石不会无警告运行)。另外,我更喜欢收到NoMethodError 而不是收到警告,但是 YMMV。 @Jwan622:您可以先编写一个示例,其中有foo = Foo.new(...),然后是用户foo。稍后,您在同一示例组中编写一个新示例,该示例还需要以相同方式实例化 Foo。此时,您要进行重构以消除重复。您可以从示例中删除 foo = Foo.new(...) 行并将其替换为 let(:foo) Foo.new(...) 而不更改示例使用 foo 的方式。但是如果你重构为before @foo = Foo.new(...) ,你还必须将示例中的引用从foo更新为@foo【参考方案2】:

使用实例变量和let() 的区别在于let()惰性求值。这意味着 let() 在它定义的方法第一次运行之前不会被评估。

beforelet 之间的区别在于let() 为您提供了一种以“级联”样式定义一组变量的好方法。通过这样做,通过简化代码,规范看起来更好一些。

【讨论】:

我明白了,这真的是优势吗?不管怎样,每个示例的代码都在运行。 IMO 更容易阅读,可读性是编程语言的一个重要因素。 Senthil - 当您使用 let() 时,它实际上不一定在每个示例中都运行。它是惰性的,因此只有在被引用时才会运行。一般来说,这并不重要,因为示例组的目的是让多个示例在一个共同的上下文中运行。 这是否意味着如果您每次都需要评估某些东西,您不应该使用let?例如在父模型上触发某些行为之前,我需要一个子模型存在于数据库中。我不一定在测试中引用该子模型,因为我正在测试父模型的行为。目前我正在使用let! 方法,但也许将设置放在before(:each) 中会更明确? @gar - 我会使用工厂(如 FactoryGirl),它允许您在实例化父级时创建那些所需的子关联。如果你这样做,那么使用 let() 或 setup 块并不重要。如果您不需要为子上下文中的每个测试使用所有内容,那么 let() 很好。安装程序应该只包含每个所需的内容。【参考方案3】:

我已经在我的 rspec 测试中完全替换了所有实例变量的使用来使用 let()。我为一位用它来教授小型 Rspec 课程的朋友编写了一个快速示例:http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

正如这里的其他一些答案所说, let() 是惰性评估的,因此它只会加载需要加载的那些。它干燥规范并使其更具可读性。事实上,我已经移植了 Rspec let() 代码以在我的控制器中使用,采用继承资源 gem 的样式。 http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

除了惰性评估之外,另一个优点是,结合 ActiveSupport::Concern 和 load-everything-in spec/support/ 行为,您可以针对您的应用程序创建您自己的规范 mini-DSL。我已经编写了一些用于针对 Rack 和 RESTful 资源进行测试的方法。

我使用的策略是Factory-everything(通过机械师+伪造/伪造者)。但是,可以将它与 before(:each) 块结合使用来为整个示例组预加载工厂,从而使规范运行得更快:http://makandra.com/notes/770-taking-advantage-of-rspec-s-let-in-before-blocks

【讨论】:

嗨 Ho-Sheng,在问这个问题之前,我实际上阅读了你的几篇博文。关于您的# spec/friendship_spec.rb# spec/comment_spec.rb 示例,您不认为它们会降低可读性吗?我不知道users 来自哪里,需要深入挖掘。 我展示该格式的前十几个人都发现它更具可读性,其中一些人开始使用它来写作。我现在使用 let() 获得了足够的规范代码,但我也遇到了其中一些问题。我发现自己要去看示例,然后从最里面的示例组开始,重新开始工作。这与使用高度元可编程环境的技能相同。 我遇到的最大问题是不小心使用了 let(:subject) 而不是 subject 。 subject() 的设置与 let(:subject) 不同,但 let(:subject) 会覆盖它。 如果您可以放手“深入”代码,那么您会发现扫描带有 let() 声明的代码要快得多。扫描代码时选择 let() 声明比找到嵌入代码中的 @variables 更容易。使用@variables,我没有一个好的“形状”,哪些行指代分配给变量,哪些行指代测试变量。使用 let(),所有的赋值都是用 let() 完成的,所以你可以通过你的声明所在的字母的形状“立即”知道。 你可以对实例变量更容易挑选出同样的论点,特别是因为一些编辑器,比如我的 (gedit) 突出显示实例变量。过去几天我一直在使用let(),除了迈伦提到的第一个优势外,我个人认为没有区别。而且我不太确定是否放手,也许是因为我很懒,而且我喜欢提前查看代码而不必打开另一个文件。感谢您的 cmets。【参考方案4】:

请务必记住,let 是惰性求值的,并且不会在其中添加副作用方法,否则您将无法从 let 更改为 before(:each) 很容易。 您可以使用 let! 代替 let,以便在每个场景之前对其进行评估。

【讨论】:

【参考方案5】:

一般来说,let() 是一种更好的语法,它可以让你不用到处输入@name 符号。但是,caveat emptor! 我发现 let() 还引入了一些细微的错误(或至少是令人头疼的问题),因为在您尝试使用它之前,该变量并不真正存在......告诉故事标志:如果在let() 之后添加puts 以查看变量是否正确,则允许规范通过,但如果没有puts,规范将失败——你已经发现了这个微妙之处。

我还发现let() 似乎并非在所有情况下都可以缓存!我把它写在我的博客上:http://technicaldebt.com/?p=1242

也许只有我一个人?

【讨论】:

let 总是记住单个示例持续时间的值。它不会记住多个示例中的值。相比之下,before(:all) 允许您在多个示例中重用已初始化的变量。 如果您想使用 let (现在似乎被认为是最佳实践),但需要立即实例化特定变量,这就是 let! 的设计目的。 relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…【参考方案6】:

let 是函数式的,因为它本质上是一个 Proc。还有它的缓存。

我马上发现了一个问题 let... 在一个正在评估更改的 Spec 块中。

let(:object) FactoryGirl.create :object

expect 
  post :destroy, id: review.id
.to change(Object, :count).by(-1)

您需要确保在您的期望块之外调用let。即您在 let 块中调用 FactoryGirl.create。我通常通过验证对象是否持久来做到这一点。

object.persisted?.should eq true

否则当第一次调用let 块时,由于延迟实例化,数据库实际上会发生变化。

更新

只是添加一个注释。小心玩code golf 或在这种情况下用这个答案 rspec 高尔夫。

在这种情况下,我只需要调用对象响应的某个方法。所以我在对象上调用_.persisted?_ 方法作为它的真值。我要做的就是实例化对象。你可以叫空吗?还是零?也。重点不是测试,而是通过调用对象来赋予它生命。

所以你不能重构

object.persisted?.should eq true

成为

object.should be_persisted 

因为对象还没有被实例化......它是懒惰的。 :)

更新 2

利用let! syntax 进行即时对象创建,这应该可以完全避免这个问题。请注意,尽管它会破坏 non banged let 的懒惰的许多目的。

此外,在某些情况下,您可能实际上想要利用 subject syntax 而不是 let,因为它可能会为您提供更多选择。

subject(:object) FactoryGirl.create :object

【讨论】:

【参考方案7】:

这里的反对声音:经过 5 年的 rspec,我不太喜欢 let

1。懒惰的评估常常使测试设置混乱

当在 setup 中声明的一些东西实际上并没有影响状态时,就很难推理 setup 了。

最终,出于沮丧,有人只是将let 更改为let!(同样的事情没有惰性评估),以使他们的规范正常工作。如果这对他们有用,就会产生一个新习惯:当一个新规范被添加到一个旧套件并且它不起作用时,作者尝试的第一个事情是在随机@添加刘海987654324@ 来电。

很快所有的性能优势都消失了。

2。特殊语法对于非 rspec 用户来说是不常见的

我更愿意向我的团队教授 Ruby,而不是 rspec 的技巧。实例变量或方法调用在本项目和其他项目中无处不在,let 语法仅在 rspec 中有用。

3。 “好处”让我们很容易忽略好的设计变化

let() 适用于我们不想一遍又一遍地创建的昂贵依赖项。 它还可以与subject 很好地搭配,让您可以干掉对多参数方法的重复调用

多次重复的昂贵依赖项和具有大签名的方法都是我们可以使代码更好的地方:

也许我可以引入一种新的抽象,将依赖项与我的其余代码隔离开来(这意味着需要它的测试更少) 可能是被测代码做的太多了 也许我需要注入更智能的对象而不是一长串原语 可能我违反了告诉-不要-问 也许可以更快地编写昂贵的代码(更罕见 - 请注意此处的过早优化)

在所有这些情况下,我可以使用 rspec 魔法的舒缓药膏来解决困难测试的症状,或者我可以尝试解决原因。我觉得过去几年我在前者上花了太多时间,现在我想要一些更好的代码。

回答最初的问题:我不想这样做,但我仍然使用let。我主要使用它来适应团队其他成员的风格(似乎世界上大多数 Rails 程序员现在都深入了解他们的 rspec 魔法,所以这种情况很常见)。有时我在向一些我无法控制的代码添加测试时使用它,或者没有时间重构为更好的抽象:即当唯一的选择是止痛药时。

【讨论】:

【参考方案8】:

Joseph 请注意——如果您在 before(:all) 中创建数据库对象,它们将不会在事务中被捕获,并且您更有可能在测试数据库中留下杂乱无章的东西。请改用before(:each)

使用 let 及其惰性求值的另一个原因是,您可以获取一个复杂的对象并通过在上下文中覆盖 let 来测试各个部分,就像在这个非常人为的示例中一样:

context "foo" do
  let(:params) do
      :foo => foo,  :bar => "bar" 
  end
  let(:foo)  "foo" 
  it "is set to foo" do
    params[:foo].should eq("foo")
  end
  context "when foo is bar" do
    let(:foo)  "bar" 
    # NOTE we didn't have to redefine params entirely!
    it "is set to bar" do
      params[:foo].should eq("bar")
    end
  end
end

【讨论】:

+1 before(:all) 错误浪费了我们开发人员很多天的时间。【参考方案9】:

“之前”默认意味着before(:each)。参考 Rspec Book,版权所有 2010,第 228 页。

before(scope = :each, options=, &block)

我使用before(:each) 为每个示例组播种一些数据,而无需调用let 方法在“it”块中创建数据。在这种情况下,“it”块中的代码更少。

如果我想要一些示例中的一些数据而不是其他示例,我使用let

before 和 let 都非常适合干燥“it”块。

为避免混淆,“let”与before(:all) 不同。 “Let”为每个示例(“it”)重新评估其方法和值,但在同一示例中的多个调用中缓存该值。你可以在这里阅读更多信息:https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

【讨论】:

【参考方案10】:

我使用let 使用上下文在我的 API 规范中测试我的 HTTP 404 响应。

为了创建资源,我使用let!。但是为了存储资源标识符,我使用let。看看它的样子:

let!(:country)    create(:country) 
let(:country_id)  country.id 
before            get "api/countries/#country_id" 

it 'responds with HTTP 200'  should respond_with(200) 

context 'when the country does not exist' do
  let(:country_id)  -1 
  it 'responds with HTTP 404'  should respond_with(404) 
end

这样可以保持规范的简洁和可读性。

【讨论】:

以上是关于何时使用 RSpec let()?的主要内容,如果未能解决你的问题,请参考以下文章

RSpec:let 和 before 块有啥区别?

RSpec 中的测试模块

方案:何时使用 let、let* 和 letrec? [复制]

在rspec中的上下文内部循环无法正确设置let变量

失败/错误:使用Rspec发布#create testing

RSPEC 让备用作用域