何时使用RSpec let()?


447

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

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

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


1
让块懒惰地求值,而在每个示例之前的块运行之前(它们总体上较慢)。使用before块取决于个人喜好(编码样式,模拟/存根...)。通常首选let块。您可以检查有关let
Nesha Zoric 18-2-8

在before挂钩中设置实例变量不是一个好习惯。请查看Betterspecs.org
艾里森(Allison)

Answers:


604

let由于以下两个原因,我总是更喜欢实例变量:

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

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


2
我真的很喜欢您提到的第一个优势,但是您能否对第三个优势进行更多说明?到目前为止,我所看到的示例(mongoid规范:github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/…)使用单行块,而且我不明白没有“ @”是怎么做到的更容易阅读。
发送,11年

6
就像我说的那样,这有点主观,但是我发现使用它let来定义所有依赖对象以及before(:each)设置所需的配置或示例所需的任何模拟/存根会很有帮助。与包含所有这些的钩子之前相比,我更喜欢这种方法。而且,let(:foo) { Foo.new }它的噪音要小一些(要点多)before(:each) { @foo = Foo.new }。这是我的用法示例:github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util / ...
Myron Marston

感谢您的示例,这确实有所帮助。
发送,11年

3
安德鲁·格林(Andrew Grimm):的确如此,但是警告可能会产生大量噪音(即,您使用的宝石不会无警告地运行)。另外,我更喜欢得到NoMethodError警告而不是警告,但YMMV。
Myron Marston

1
@ Jwan622:您可能会先编写一个示例,该示例包含foo = Foo.new(...),然后foo在以后的行中使用用户。稍后,您在相同的示例组中编写了一个新示例,该示例也需要以Foo相同的方式实例化。此时,您需要重构以消除重复。您可以foo = Foo.new(...)从示例中删除这些行,并以不替换let(:foo) { Foo.new(...) }示例使用方式的方式替换foo。但是,如果要重构,则before { @foo = Foo.new(...) }还必须将示例中的引用更新foo@foo
迈伦·马斯顿,

82

使用实例变量和之间的差异let()在于,let()懒评估。这意味着let()直到第一次运行它定义的方法时,才对其进行评估。

之间的区别beforeletlet()给你定义的“级联”的风格一组变量的一个很好的方式。这样,通过简化代码,规范看起来会更好一些。


1
我知道,这真的是优势吗?无论如何,每个示例都在运行该代码。
2011年

2
读取IMO更容易,并且可读性是编程语言中的重要因素。
Mike Lewis,

4
Senthil-使用let()时,不一定在每个示例中都运行它。它很懒,因此只有在被引用的情况下才可以运行。一般来说,这无关紧要,因为示例组的目的是让多个示例在公共上下文中运行。
David Chelimsky 2011年

1
那么这是否意味着let如果您需要每次进行评估,就不应该使用?例如,在父模型上触发某些行为之前,我需要一个子模型出现在数据库中。我不必在测试中引用该子模型,因为我正在测试父模型的行为。目前,我正在使用该let!方法,但是将其设置为可能会更加明确before(:each)
gar

2
@gar-我将使用Factory(例如FactoryGirl),该工厂允许您在实例化父级时创建那些必需的子级关联。如果采用这种方式,那么使用let()或setup块并不重要。如果您不需要为子上下文中的每个测试使用一切,那么let()会很不错。安装程序应该只包含每个组件所需的组件。
哈蒙2012年

17

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

就像这里的其他答案所说的那样,let()是延迟计算的,因此它只会加载需要加载的内容。它使规范变干并使其更具可读性。实际上,我已经继承了Rspec let()代码,以便以Inherited_resource gem的样式在控制器中使用。http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

与惰性评估一起,另一个优点是,与ActiveSupport :: Concern结合使用,以及负载不一的规范/支持/行为,您可以创建自己的针对特定应用的规范mini-DSL。我已经写了一些针对Rack和RESTful资源的测试。

我使用的策略是“工厂一切”(通过机械师+伪造/ Faker)。但是,可以将它与before(:each)块结合使用来为整个示例组集预加载工厂,从而使规范运行得更快:http : //makandra.com/notes/770-taking-advantage RSpec放在块之前


嗨,何晟,在问这个问题之前,我实际上已经阅读了您的几篇博客文章。关于您# spec/friendship_spec.rb# spec/comment_spec.rb示例,您是否认为它们使可读性降低?我不知道从何users而来,需要深入研究。
发送,11年

我向最初展示过这种格式的人们展示了该格式更具可读性,其中一些人开始使用该格式进行书写。我现在使用let()有了足够的规范代码,我也遇到了一些问题。我发现自己要去看示例,然后从最里面的示例组开始,自己备份。它与使用高度元编程的环境具有相同的技能。
何晟晨

2
我遇到的最大难题是意外地使用let(:subject){}而不是subject {}。subject()的设置与let(:subject)的设置不同,但是let(:subject)将覆盖它。
何晟晨

1
如果您可以放手“深入”到代码中,那么您会发现用let()声明扫描代码快得多。扫描代码时挑选出let()声明比查找嵌入到代码中的@variables更容易。使用@variables时,我没有一个好的“形状”,其中的线表示分配给变量,而线表示测试变量。使用let(),所有分配都通过let()完成,因此您可以通过声明所在的字母的形状“立即”知道它们。
何晟晨

1
您可以提出关于实例变量更容易挑选的相同论点,尤其是因为某些编辑器(例如我的(gedit))突出显示实例变量。let()过去几天我一直在使用,但我个人没有什么区别,除了Myron提到的第一个优势。而且我不太确定要放手,什么也不放手,也许是因为我很懒,而且我喜欢预先查看代码而不必打开另一个文件。感谢您的意见。
发送,11年

13

重要的是要记住,let是惰性计算的,不要在其中放入副作用方法,否则您将无法轻松地从let更改为before(:each)。您可以使用let!而不是让它在每个场景之前进行评估。


8

通常,这let()是一种更好的语法,它可以节省您@name在各处输入符号的麻烦。但是,告诫者!我发现let()还引入了微妙的错误(或者至少挠头),因为变量实际上并不存在,直到您尝试使用它......告诉故事标志:如果将一个putslet()看到的变量是正确允许规范通过,但没有puts规格失败-您已经发现了这种微妙之处。

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

也许就是我?


9
let始终在单个示例的持续时间内记住该值。它不会在多个示例中记住该值。before(:all)相反,允许您在多个示例中重复使用初始化的变量。
迈伦·马斯顿

2
如果您想使用let(现在似乎被认为是最佳实践),但是需要立即实例化一个特定的变量,那let!就是设计目的。 relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…–
雅各布(Jacob)

6

let本质上是一个Proc。还对其进行了缓存。

我马上就发现了一个陷阱……在评估变更的Spec块中。

let(:object) {FactoryGirl.create :object}

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

您需要确保let在您的Expect块之外调用。也就是说,您正在调用FactoryGirl.createlet块。我通常通过验证对象是否持久来做到这一点。

object.persisted?.should eq true

否则,let由于延迟实例化,第一次调用该块时,实际上将发生数据库更改。

更新资料

只是添加一个注释。请谨慎打代码高尔夫,或者在这种情况下使用此答案进行rspec高尔夫。

在这种情况下,我只需要调用对象响应的某些方法即可。因此,我_.persisted?在对象上调用_方法作为其真实性。我要做的就是实例化该对象。你可以叫空吗?还是零?太。关键不是测试,而是通过调用对象来延长对象寿命。

所以你无法重构

object.persisted?.should eq true

成为

object.should be_persisted 

因为该对象尚未实例化...它是惰性的。:)

更新2

利用放手!即时对象创建的语法,应该完全避免此问题。请注意,尽管这样做会打败非猛烈放任的懒惰的许多目的。

同样,在某些情况下,您实际上可能希望利用主题语法而不是让它使用,因为它可能会给您带来更多选择。

subject(:object) {FactoryGirl.create :object}

2

约瑟夫(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
+1 before(:all)错误浪费了我们开发人员很多时间。
Michael Durrant 2014年

1

默认情况下,“之前”暗含before(:each)。参见Rspec图书,版权2010,第228页。

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

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

let如果在某些示例中需要一些数据,而在其他示例中则不需要,我会使用。

之前和让它们都非常适合干燥“ it”块。

为避免混淆,“ let”与相同before(:all)。“让”为每个示例(“ it”)重新评估其方法和值,但在同一示例中跨多个调用缓存该值。您可以在此处了解更多信息:https : //www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let


1

在这里有不同的声音:经过5年的rspec我不太喜欢let

1.懒惰的评估通常会使测试设置混乱

当在安装程序中声明的某些事物实际上并未影响状态,而另一些事物确实在影响状态时,就很难对安装程序进行推理了。

最终,出于无奈有人只是改变letlet!(不懒评价同一件事),以获得他们的规范工作。如果这对他们有用,那么就会养成新的习惯:当将新规范添加到较旧的套件中而行不通时,作者尝试的第一件事就是在随机中添加刘海let调用中。

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

2.特殊语法对于非rspec用户是不寻常的

我更愿意向我的团队教Ruby,而不是rspec的技巧。实例变量或方法调用在该项目以及其他项目中都非常有用,let语法仅在rspec中有用。

3.“好处”使我们可以轻松忽略良好的设计更改

let()对于我们不想一遍又一遍创建的昂贵依赖项很有用。它也与subject,使您可以避免重复调用多参数方法

昂贵的依赖关系多次重复出现,并且带有大签名的方法都是我们可以使代码变得更好的两个方面:

  • 也许我可以引入一个新的抽象,将依赖与我的其余代码隔离开(这意味着更少的测试需要它)
  • 也许正在测试的代码做得太多
  • 也许我需要注入更智能的对象,而不是一长串的基元
  • 也许我违反了告诫
  • 也许可以使昂贵的代码更快(稀有-请提防过早的优化)

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

要回答原始问题:我不愿意,但是我仍然使用let。我主要使用它来适应团队其他成员的风格(似乎世界上大多数Rails程序员现在都已经深入到他们的rspec魔术中了,这种情况经常出现)。有时,当我在一些我无法控制的代码中添加测试或没有时间重构更好的抽象时使用它:即,唯一的选择是止痛药时。


0

我用 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

这样可以使规范保持清洁和可读性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.