ChatGPT解决这个技术问题 Extra ChatGPT

何时使用 RSpec let()?

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

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

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

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

A
Armand Fardeau

出于以下几个原因,我总是更喜欢 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
s
steakunderscore

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

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


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

我已经在我的 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(通过Machinist+Forgery/Faker)。但是,可以将它与 before(:each) 块结合使用来为整组示例组预加载工厂,从而使规范运行得更快:http://makandra.com/notes/770-taking-advantage-of-rspec-s-let-in-before-blocks


嘿何生,在问这个问题之前,我实际上阅读了你的几篇博客文章。关于您的 # 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(),除了 Myron 提到的第一个优势之外,我个人看不出有什么不同。而且我不太确定是否放手,也许是因为我很懒,而且我喜欢提前查看代码而不必打开另一个文件。感谢您的意见。
P
Pikachu

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


J
Jon Kern

一般来说,let() 是一种更好的语法,它可以让您在所有地方都输入 @name 符号。但是,警告购买者!我发现 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/…
D
Danilo Cândido

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}

i
iftheshoefritz

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

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

当在 setup 中声明的某些东西实际上并没有影响状态时,很难对 setup 进行推理,而另一些却是。

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

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

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

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

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

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

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

也许我可以引入一个新的抽象,将依赖项与我的其余代码隔离开来(这意味着更少的测试需要它)

也许被测代码做的太多了

也许我需要注入更智能的对象而不是一长串原语

也许我违反了告诉-不要-问

也许昂贵的代码可以变得更快(更罕见 - 在这里提防过早的优化)

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

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


D
Danilo Cândido

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) 错误浪费了我们开发人员很多天的时间。
D
Danilo Cândido

默认情况下,“之前”意味着 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


V
Vinicius Brasil

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

这使规格保持干净和可读。


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅