何时使用RSpec let()?

sen*_*hil 442 ruby rspec

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

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

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

Myr*_*ton 600

我总是喜欢let一个实例变量,原因有两个:

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

相关链接可在此处找到:http://www.betterspecs.org/#let

  • 正如我所说,它有点主观,但我发现使用`let`来定义所有依赖对象,并使用`before(:each)`来设置所需的配置或示例所需的任何模拟/存根是有帮助的. .我更喜欢这个包含所有这一切之前的一个大的钩子.另外,`let(:foo){Foo.new}`不那么嘈杂(而且更重要)然后`before(:each){@foo = Foo.new}`.以下是我如何使用它的示例:https://github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/hooks_spec.rb#L9-16 (6认同)
  • Andrew Grimm:是的,但是警告可能会产生大量的噪音(即来自您使用的不会无警告的宝石).另外,我更喜欢得到一个'NoMethodError`来获得警告,但YMMV. (3认同)
  • 我真的很喜欢你提到的第一个优势,但你能解释一下第三个吗?到目前为止,我见过的例子(mongoid规格:https://github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/config_spec.rb)使用单行块,我不知道怎么回事拥有"@"使其更容易阅读. (2认同)

Mik*_*wis 82

使用实例变量和之间的差异let()在于,let()懒评估.这意味着let()在第一次运行它定义的方法之前不会对其进行求值.

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

  • Senthil - 当你使用let()时,它实际上并不一定在每个例子中运行.它是懒惰的,所以它只在被引用时运行.一般来说,这并不重要,因为示例组的要点是在共同的上下文中运行几个示例. (4认同)
  • 读IMO更容易,可读性是编程语言的一个重要因素. (2认同)
  • @gar - 我会使用Factory(如FactoryGirl),它允许您在实例化父项时创建所需的子关联.如果你这样做,那么使用let()或设置块并不重要.如果您不需要在子上下文中为每个测试使用EVERYTHING,则let()很好.安装程序应该只包含每个安装程序所需的内容. (2认同)

Ho-*_*iao 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,以及load-everything-in规范/支持/行为,您可以创建特定于您的应用程序的自己的spec mini-DSL.我已经编写过针对Rack和RESTful资源进行测试的文章.

我使用的策略是Factory-everything(通过Machinist + Forgery/Faker).但是,可以将它与before(:each)块结合使用,为整个示例组预加载工厂,从而使规范运行得更快:http://makandra.com/notes/770-taking-advantage -of-rspec的-S-LET-在前方的块

  • 我遇到的最大问题是偶然使用let(:subject){}而不是subject {}.subject()的设置与let(:subject)不同,但let(:subject)将覆盖它. (2认同)

pis*_*ruk 13

重要的是要记住let是懒惰评估而不是在其中放置副作用方法,否则你将无法轻易地从let更改为before(:each).你可以用let!而不是让它在每个场景之前进行评估.


ift*_*itz 11

这里有不同的声音:使用了 5 年 rspec 我不太喜欢let

1. 惰性评估常常使测试设置变得混乱

当设置中声明的某些内容实际上并不影响状态,而其他内容则影响状态时,就很难对设置进行推理。

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

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

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

我宁愿向我的团队教授 Ruby,也不愿教 rspec 的技巧。实例变量或方法调用在本项目和其他项目中的任何地方都很有用,let语法仅在 rspec 中有用。

3.“好处”让我们很容易忽视好的设计变更

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

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

  • 也许我可以引入一个新的抽象,将依赖项与代码的其余部分隔离(这意味着需要它的测试更少)
  • 也许被测试的代码做了太多的事情
  • 也许我需要注入更智能的对象而不是一长串原语
  • 也许我违反了“只说不问”的原则
  • 也许昂贵的代码可以变得更快(罕见 - 此处要注意过早优化)

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

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


Jon*_*ern 8

通常,它let()是一种更好的语法,它可以节省您@name在整个地方键入符号.但是,请注意!我发现let()还引入了微妙的错误(或者至少是头部划痕),因为变量在您尝试使用之前并不存在...告诉故事标志:如果在添加puts之后let()看到变量是正确的则允许规范传递,但没有puts规范失败 - 你已经找到了这个微妙.

我也发现let()在所有情况下似乎都没有缓存!我在我的博客中写了这篇文章:http://technicaldebt.com/?p = 1242

也许只是我?

  • `let`总是记住单个例子持续时间的值.它没有记住多个示例中的值.相反,`before(:all)`允许您在多个示例中重用已初始化的变量. (9认同)
  • 如果你想使用let(现在似乎被认为是最佳实践),但需要立即实例化一个特定的变量,那就是`let!`的设计目的.https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let (2认同)

eng*_*ave 6

let是功能性的,因为它本质上是一个Proc.也是它的缓存.

我得到的一个问题是......在一个正在评估变化的Spec块中.

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

expect {
  post :destroy, id: review.id
}.to change(Object, :count).by(-1)
Run Code Online (Sandbox Code Playgroud)

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

object.persisted?.should eq true
Run Code Online (Sandbox Code Playgroud)

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

更新

只需添加备注.小心打码高尔夫或在这种情况下rspec高尔夫与这个答案.

在这种情况下,我只需要调用一个对象响应的方法.所以我_.persisted?在对象上调用_方法作为它的真实.我要做的就是实例化对象.你可以打电话给空吗?还是没有?太.关键不在于测试,而是通过调用它来引导生命.

所以你无法重构

object.persisted?.should eq true
Run Code Online (Sandbox Code Playgroud)

成为

object.should be_persisted 
Run Code Online (Sandbox Code Playgroud)

因为对象尚未实例化...它的懒惰.:)

更新2

利用让!即时对象创建的语法,应该完全避免这个问题.注意虽然它会打败很多非撞击让懒惰的目的.

此外,在某些情况下,您可能实际上想要利用主题语法而不是let,因为它可能会为您提供其他选项.

subject(:object) {FactoryGirl.create :object}
Run Code Online (Sandbox Code Playgroud)