我倾向于使用 before 块来设置实例变量。然后我在我的示例中使用这些变量。我最近遇到了let()
。根据 RSpec 文档,它用于
... 定义一个记忆化的辅助方法。该值将在同一示例中的多个调用中缓存,但不会跨示例缓存。
这与在前块中使用实例变量有何不同?还有什么时候应该使用 let()
和 before()
?
出于以下几个原因,我总是更喜欢 let
而不是实例变量:
实例变量在引用时立即存在。这意味着,如果您对实例变量的拼写粗心大意,则会创建一个新变量并将其初始化为 nil,这可能会导致细微的错误和误报。因为 let 创建了一个方法,当你拼错它时你会得到一个 NameError,我觉得这更可取。它也使重构规范变得更加容易。
before(:each) 挂钩将在每个示例之前运行,即使该示例不使用挂钩中定义的任何实例变量。这通常没什么大不了的,但是如果实例变量的设置需要很长时间,那么您就是在浪费周期。对于 let 定义的方法,初始化代码只有在示例调用时才会运行。
您可以将示例中的局部变量直接重构为 let,而无需更改示例中的引用语法。如果重构为实例变量,则必须更改示例中引用对象的方式(例如添加@)。
这有点主观,但正如 Mike Lewis 指出的那样,我认为它使规范更易于阅读。我喜欢用 let 定义我所有的依赖对象并保持我的 it 块简洁明了的组织。
可以在此处找到相关链接:http://www.betterspecs.org/#let
使用实例变量和 let()
的区别在于 let()
是惰性求值。这意味着在它定义的方法第一次运行之前,不会评估 let()
。
before
和 let
之间的区别在于 let()
为您提供了一种以“级联”样式定义一组变量的好方法。通过这样做,通过简化代码,规范看起来会更好一些。
let
?例如,在父模型上触发某些行为之前,我需要一个子模型存在于数据库中。我不一定在测试中引用该子模型,因为我正在测试父模型的行为。目前我正在使用 let!
方法,但也许将设置放在 before(:each)
中会更明确?
我已经在我的 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()
,除了 Myron 提到的第一个优势之外,我个人看不出有什么不同。而且我不太确定是否放手,也许是因为我很懒,而且我喜欢提前查看代码而不必打开另一个文件。感谢您的意见。
重要的是要记住 let 是惰性求值的,并且不会在其中放置副作用方法,否则您将无法轻松地从 let 更改为 before(:each) 。你可以使用让!而不是 let 以便在每个场景之前对其进行评估。
一般来说,let()
是一种更好的语法,它可以让您在所有地方都输入 @name
符号。但是,警告购买者!我发现 let()
还引入了一些细微的错误(或至少是令人头疼的问题),因为在您尝试使用它之前该变量并不真正存在... 说明:如果在 let()
之后添加 puts
以查看变量是否正确允许规范通过,但没有 puts
规范失败 - 您已经发现了这个微妙之处。
我还发现 let()
似乎并非在所有情况下都缓存!我把它写在我的博客上:http://technicaldebt.com/?p=1242
也许只有我一个人?
let
始终记住单个示例持续时间的值。它不会记住多个示例中的值。相比之下,before(:all)
允许您在多个示例中重用已初始化的变量。
let!
的设计目的。 relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
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}
反对的声音在这里:经过 5 年的 rspec,我不太喜欢 let
。
1. 懒惰的评估经常使测试设置混乱
当在 setup 中声明的某些东西实际上并没有影响状态时,很难对 setup 进行推理,而另一些却是。
最终,出于挫败感,有人只是将 let
更改为 let!
(同样的事情没有惰性评估)以使他们的规范正常工作。如果这对他们有用,就会产生一个新习惯:当一个新规范被添加到一个旧套件并且它不起作用时,作者尝试的第一个事情是在随机 { 1} 电话。
很快所有的性能优势都消失了。
2. 特殊语法对于非 rspec 用户来说是不常见的
我宁愿将 Ruby 教给我的团队,也不愿教 rspec 的技巧。实例变量或方法调用在此项目和其他项目中的任何地方都很有用,let
语法仅在 rspec 中有用。
3.“好处”让我们很容易忽略好的设计变化
let()
适用于我们不想一遍又一遍地创建的昂贵依赖项。它还可以很好地与 subject
配对,让您可以干掉对多参数方法的重复调用
多次重复的昂贵依赖项和具有大签名的方法都是我们可以使代码更好的地方:
也许我可以引入一个新的抽象,将依赖项与我的其余代码隔离开来(这意味着更少的测试需要它)
也许被测代码做的太多了
也许我需要注入更智能的对象而不是一长串原语
也许我违反了告诉-不要-问
也许昂贵的代码可以变得更快(更罕见 - 在这里提防过早的优化)
在所有这些情况下,我可以使用 rspec 魔法的舒缓香膏来解决困难测试的症状,或者我可以尝试解决原因。我觉得过去几年我在前者上花了太多时间,现在我想要一些更好的代码。
回答最初的问题:我不想这样做,但我仍然使用 let
。我大部分使用它来适应团队其他成员的风格(似乎世界上大多数 Rails 程序员现在都深入了解他们的 rspec 魔法,所以这种情况很常见)。有时我在向一些我无法控制的代码添加测试时使用它,或者没有时间重构为更好的抽象:即当唯一的选择是止痛药时。
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
默认情况下,“之前”意味着 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
我使用 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
这使规格保持干净和可读。
let
定义所有依赖对象并使用before(:each)
设置所需的配置或示例所需的任何模拟/存根会很有帮助。我更喜欢这个,而不是一个包含所有这些的大钩子。此外,let(:foo) { Foo.new }
比before(:each) { @foo = Foo.new }
噪音更小(而且更重要)。这是我如何使用它的示例:github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/…NoMethodError
而不是收到警告,但 YMMV。foo = Foo.new(...)
,然后是用户foo
。稍后,您在同一个示例组中编写一个新示例,该示例也需要以相同方式实例化Foo
。此时,您要进行重构以消除重复。您可以从示例中删除foo = Foo.new(...)
行并将其替换为let(:foo) { Foo.new(...) }
,而无需更改示例使用foo
的方式。但是,如果您重构为before { @foo = Foo.new(...) }
,您还必须将示例中的引用从foo
更新为@foo
。